//===- VectorUnrollDistribute.cpp - patterns to do vector unrolling -------===//
//
// 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 patterns to do vector unrolling and vector distribution.
//
//===----------------------------------------------------------------------===//

#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Utils/IndexingUtils.h"
#include "mlir/Dialect/Vector/Transforms/LoweringPatterns.h"
#include "mlir/Dialect/Vector/Transforms/VectorTransforms.h"
#include "mlir/Interfaces/VectorInterfaces.h"
#include "llvm/ADT/MapVector.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/DebugLog.h"
#include "llvm/Support/InterleavedRange.h"
#include <optional>

#define DEBUG_TYPE "vector-unroll"

using namespace mlir;
using namespace mlir::vector;

SmallVector<Value> mlir::vector::sliceTransferIndices(
    ArrayRef<int64_t> elementOffsets, ArrayRef<Value> indices,
    AffineMap permutationMap, Location loc, OpBuilder &builder) {
  MLIRContext *ctx = builder.getContext();
  auto isBroadcast = [](AffineExpr expr) {
    if (auto constExpr = dyn_cast<AffineConstantExpr>(expr))
      return constExpr.getValue() == 0;
    return false;
  };
  // Compute 'sliceIndices' by adding 'sliceOffsets[i]' to 'indices[i]'.
  SmallVector<Value> slicedIndices(indices);
  for (const auto &dim : llvm::enumerate(permutationMap.getResults())) {
    int64_t elementOffset = elementOffsets[dim.index()];
    if (isBroadcast(dim.value()) || elementOffset == 0)
      continue;
    unsigned pos = cast<AffineDimExpr>(dim.value()).getPosition();
    auto expr = getAffineDimExpr(0, builder.getContext()) +
                getAffineConstantExpr(elementOffset, ctx);
    auto map = AffineMap::get(/*dimCount=*/1, /*symbolCount=*/0, expr);
    slicedIndices[pos] =
        affine::AffineApplyOp::create(builder, loc, map, indices[pos]);
  }
  return slicedIndices;
}

// Compute the new indices by adding `offsets` to `originalIndices`.
// If m < n (m = offsets.size(), n = originalIndices.size()),
// then only the trailing m values in `originalIndices` are updated.
static SmallVector<Value> sliceLoadStoreIndices(PatternRewriter &rewriter,
                                                Location loc,
                                                OperandRange originalIndices,
                                                ArrayRef<int64_t> offsets) {
  assert(offsets.size() <= originalIndices.size() &&
         "Offsets should not exceed the number of original indices");
  SmallVector<Value> indices(originalIndices);

  auto start = indices.size() - offsets.size();
  for (auto [i, offset] : llvm::enumerate(offsets)) {
    if (offset != 0) {
      indices[start + i] = arith::AddIOp::create(
          rewriter, loc, originalIndices[start + i],
          arith::ConstantIndexOp::create(rewriter, loc, offset));
    }
  }
  return indices;
}

// Clones `op` into a new operations that takes `operands` and returns
// `resultTypes`.
static Operation *cloneOpWithOperandsAndTypes(OpBuilder &builder, Location loc,
                                              Operation *op,
                                              ArrayRef<Value> operands,
                                              ArrayRef<Type> resultTypes) {
  return builder.create(loc, op->getName().getIdentifier(), operands,
                        resultTypes, op->getAttrs());
}

/// Return the target shape for unrolling for the given `op`. Return
/// std::nullopt if the op shouldn't be or cannot be unrolled.
static std::optional<SmallVector<int64_t>>
getTargetShape(const vector::UnrollVectorOptions &options, Operation *op) {
  LDBG() << "Get unroll shape for op " << op->getName().getStringRef();
  if (options.filterConstraint && failed(options.filterConstraint(op))) {
    LDBG() << "--no filter constraint -> BAIL";
    return std::nullopt;
  }
  assert(options.nativeShape &&
         "vector unrolling expects the native shape or native"
         "shape call back function to be set");
  auto unrollableVectorOp = dyn_cast<VectorUnrollOpInterface>(op);
  if (!unrollableVectorOp) {
    LDBG() << "--not an unrollable op -> BAIL";
    return std::nullopt;
  }
  auto maybeUnrollShape = unrollableVectorOp.getShapeForUnroll();
  if (!maybeUnrollShape) {
    LDBG() << "--could not get shape of op " << *op << " -> BAIL";
    return std::nullopt;
  }
  LDBG() << "--vector op shape: " << llvm::interleaved(*maybeUnrollShape);

  std::optional<SmallVector<int64_t>> targetShape = options.nativeShape(op);
  if (!targetShape) {
    LDBG() << "--no unrolling target shape defined " << *op << "-> SKIP";
    return std::nullopt;
  }
  LDBG() << "--target shape: " << llvm::interleaved(*targetShape);

  auto maybeShapeRatio = computeShapeRatio(*maybeUnrollShape, *targetShape);
  if (!maybeShapeRatio) {
    LDBG() << "--could not compute integral shape ratio -> BAIL";
    return std::nullopt;
  }
  if (llvm::all_of(*maybeShapeRatio, [](int64_t v) { return v == 1; })) {
    LDBG() << "--no unrolling needed -> SKIP";
    return std::nullopt;
  }
  LDBG() << "--found an integral shape ratio to unroll to -> SUCCESS";
  return targetShape;
}

static SmallVector<int64_t>
getUnrollOrder(unsigned numLoops, Operation *op,
               const vector::UnrollVectorOptions &options) {
  SmallVector<int64_t> loopOrder =
      llvm::to_vector(llvm::seq<int64_t>(0, static_cast<int64_t>(numLoops)));
  if (options.traversalOrderCallback != nullptr) {
    std::optional<SmallVector<int64_t>> order =
        options.traversalOrderCallback(op);
    if (order) {
      loopOrder = std::move(*order);
    }
  }
  return loopOrder;
}

namespace {

struct UnrollTransferReadPattern
    : public OpRewritePattern<vector::TransferReadOp> {
  UnrollTransferReadPattern(MLIRContext *context,
                            const vector::UnrollVectorOptions &options,
                            PatternBenefit benefit = 1)
      : OpRewritePattern<vector::TransferReadOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::TransferReadOp readOp,
                                PatternRewriter &rewriter) const override {
    // TODO: support 0-d corner case.
    if (readOp.getTransferRank() == 0)
      return failure();
    if (readOp.getMask())
      return failure();
    auto targetShape = getTargetShape(options, readOp);
    if (!targetShape)
      return failure();
    auto sourceVectorType = readOp.getVectorType();
    SmallVector<int64_t> strides(targetShape->size(), 1);
    Location loc = readOp.getLoc();
    ArrayRef<int64_t> originalSize = sourceVectorType.getShape();

    // Prepare the result vector;
    Value result =
        arith::ConstantOp::create(rewriter, loc, sourceVectorType,
                                  rewriter.getZeroAttr(sourceVectorType));
    auto targetType =
        VectorType::get(*targetShape, sourceVectorType.getElementType());
    SmallVector<Value> originalIndices(readOp.getIndices().begin(),
                                       readOp.getIndices().end());
    SmallVector<int64_t> loopOrder =
        getUnrollOrder(originalSize.size(), readOp, options);
    for (SmallVector<int64_t> elementOffsets :
         StaticTileOffsetRange(originalSize, *targetShape, loopOrder)) {
      SmallVector<Value> indices =
          sliceTransferIndices(elementOffsets, originalIndices,
                               readOp.getPermutationMap(), loc, rewriter);
      auto slicedRead = vector::TransferReadOp::create(
          rewriter, loc, targetType, readOp.getBase(), indices,
          readOp.getPermutationMapAttr(), readOp.getPadding(), readOp.getMask(),
          readOp.getInBoundsAttr());

      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, slicedRead, result, elementOffsets, strides);
    }
    rewriter.replaceOp(readOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollTransferWritePattern
    : public OpRewritePattern<vector::TransferWriteOp> {
  UnrollTransferWritePattern(MLIRContext *context,
                             const vector::UnrollVectorOptions &options,
                             PatternBenefit benefit = 1)
      : OpRewritePattern<vector::TransferWriteOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::TransferWriteOp writeOp,
                                PatternRewriter &rewriter) const override {
    // TODO: support 0-d corner case.
    if (writeOp.getTransferRank() == 0)
      return failure();

    if (writeOp.getMask())
      return failure();
    auto targetShape = getTargetShape(options, writeOp);
    if (!targetShape)
      return failure();
    auto sourceVectorType = writeOp.getVectorType();
    SmallVector<int64_t> strides(targetShape->size(), 1);
    Location loc = writeOp.getLoc();
    ArrayRef<int64_t> originalSize = sourceVectorType.getShape();
    // Bail-out if rank(source) != rank(target). The main limitation here is the
    // fact that `ExtractStridedSlice` requires the rank for the input and
    // output to match. If needed, we can relax this later.
    if (originalSize.size() != targetShape->size())
      return rewriter.notifyMatchFailure(
          writeOp,
          "expected source input vector rank to match target shape rank");

    SmallVector<Value> originalIndices(writeOp.getIndices().begin(),
                                       writeOp.getIndices().end());
    SmallVector<int64_t> loopOrder =
        getUnrollOrder(originalSize.size(), writeOp, options);
    Value resultTensor;
    for (SmallVector<int64_t> elementOffsets :
         StaticTileOffsetRange(originalSize, *targetShape, loopOrder)) {
      Value slicedVector = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, writeOp.getVector(), elementOffsets, *targetShape, strides);
      SmallVector<Value> indices =
          sliceTransferIndices(elementOffsets, originalIndices,
                               writeOp.getPermutationMap(), loc, rewriter);
      Operation *slicedWrite = vector::TransferWriteOp::create(
          rewriter, loc, slicedVector,
          resultTensor ? resultTensor : writeOp.getBase(), indices,
          writeOp.getPermutationMapAttr(), writeOp.getInBoundsAttr());
      // For the tensor case update the destination for the next transfer write.
      if (!slicedWrite->getResults().empty())
        resultTensor = slicedWrite->getResult(0);
    }
    if (resultTensor)
      rewriter.replaceOp(writeOp, resultTensor);
    else
      rewriter.eraseOp(writeOp);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct OffsetMapInfo {
  static unsigned getHashValue(const SmallVector<int64_t> &v) {
    return static_cast<unsigned>(llvm::hash_combine_range(v));
  }

  static bool isEqual(const SmallVector<int64_t> &lhs,
                      const SmallVector<int64_t> &rhs) {
    return lhs == rhs;
  }
};

struct UnrollContractionPattern
    : public OpRewritePattern<vector::ContractionOp> {
  UnrollContractionPattern(MLIRContext *context,
                           const vector::UnrollVectorOptions &options,
                           PatternBenefit benefit = 1)
      : OpRewritePattern<vector::ContractionOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::ContractionOp contractOp,
                                PatternRewriter &rewriter) const override {
    auto targetShape = getTargetShape(options, contractOp);
    if (!targetShape)
      return failure();
    auto dstVecType = cast<VectorType>(contractOp.getResultType());
    SmallVector<int64_t> originalSize = *contractOp.getShapeForUnroll();

    Location loc = contractOp.getLoc();
    unsigned accIndex = vector::ContractionOp::getAccOperandIndex();
    AffineMap dstAffineMap = contractOp.getIndexingMapsArray()[accIndex];
    llvm::MapVector<
        SmallVector<int64_t>, Value,
        llvm::DenseMap<SmallVector<int64_t>, unsigned, OffsetMapInfo>>
        accCache;

    SmallVector<int64_t> loopOrder = getUnrollOrder(
        contractOp.getIteratorTypes().size(), contractOp, options);

    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalSize, *targetShape, loopOrder)) {
      SmallVector<Value> slicesOperands(contractOp.getNumOperands());

      // Helper to compute the new shape of each operand and extract the slice.
      auto extractOperand = [&](unsigned index, Value operand,
                                AffineMap permutationMap,
                                ArrayRef<int64_t> operandOffets) {
        SmallVector<int64_t> operandShape = applyPermutationMap(
            permutationMap, ArrayRef<int64_t>(*targetShape));
        SmallVector<int64_t> operandStrides(operandOffets.size(), 1);
        slicesOperands[index] =
            rewriter.createOrFold<vector::ExtractStridedSliceOp>(
                loc, operand, operandOffets, operandShape, operandStrides);
      };

      // Extract the new lhs operand.
      AffineMap lhsPermutationMap = contractOp.getIndexingMapsArray()[0];
      SmallVector<int64_t> lhsOffets =
          applyPermutationMap(lhsPermutationMap, ArrayRef<int64_t>(offsets));
      extractOperand(0, contractOp.getLhs(), lhsPermutationMap, lhsOffets);

      // Extract the new rhs operand.
      AffineMap rhsPermutationMap = contractOp.getIndexingMapsArray()[1];
      SmallVector<int64_t> rhsOffets =
          applyPermutationMap(rhsPermutationMap, ArrayRef<int64_t>(offsets));
      extractOperand(1, contractOp.getRhs(), rhsPermutationMap, rhsOffets);

      AffineMap accPermutationMap = contractOp.getIndexingMapsArray()[2];
      SmallVector<int64_t> accOffets =
          applyPermutationMap(accPermutationMap, ArrayRef<int64_t>(offsets));
      // If a version of the accumulator has already been computed, use it
      // otherwise extract the first version from the original operand.
      auto *accIt = accCache.find(accOffets);
      if (accIt != accCache.end())
        slicesOperands[2] = accIt->second;
      else
        extractOperand(2, contractOp.getAcc(), accPermutationMap, accOffets);

      SmallVector<int64_t> dstShape =
          applyPermutationMap(dstAffineMap, ArrayRef<int64_t>(*targetShape));
      auto targetType = VectorType::get(dstShape, dstVecType.getElementType());
      Operation *newOp = cloneOpWithOperandsAndTypes(
          rewriter, loc, contractOp, slicesOperands, targetType);

      SmallVector<int64_t> dstOffets =
          applyPermutationMap(dstAffineMap, ArrayRef<int64_t>(offsets));
      // Save the accumulated value untill all the loops are unrolled since
      // reduction loop keep updating the accumulator.
      accCache[dstOffets] = newOp->getResult(0);
    }
    // Assemble back the accumulator into a single vector.
    Value result = arith::ConstantOp::create(rewriter, loc, dstVecType,
                                             rewriter.getZeroAttr(dstVecType));
    for (const auto &it : accCache) {
      SmallVector<int64_t> dstStrides(it.first.size(), 1);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, it.second, result, it.first, dstStrides);
    }
    rewriter.replaceOp(contractOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollMultiReductionPattern
    : public OpRewritePattern<vector::MultiDimReductionOp> {
  UnrollMultiReductionPattern(MLIRContext *context,
                              const vector::UnrollVectorOptions &options,
                              PatternBenefit benefit = 1)
      : OpRewritePattern<vector::MultiDimReductionOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::MultiDimReductionOp reductionOp,
                                PatternRewriter &rewriter) const override {
    std::optional<SmallVector<int64_t>> targetShape =
        getTargetShape(options, reductionOp);
    if (!targetShape)
      return failure();
    SmallVector<int64_t> originalSize = *reductionOp.getShapeForUnroll();
    Location loc = reductionOp.getLoc();
    auto resultType = reductionOp->getResult(0).getType();

    // Handle scalar result case: all dimensions are reduced.
    // Each source tile is reduced to a scalar, and partial results are
    // chained through the accumulator operand.
    if (resultType.isIntOrFloat()) {
      Value accumulator = reductionOp.getAcc();
      for (SmallVector<int64_t> offsets :
           StaticTileOffsetRange(originalSize, *targetShape)) {
        SmallVector<int64_t> operandStrides(offsets.size(), 1);
        Value slicedOperand =
            rewriter.createOrFold<vector::ExtractStridedSliceOp>(
                loc, reductionOp.getSource(), offsets, *targetShape,
                operandStrides);
        Operation *newOp = cloneOpWithOperandsAndTypes(
            rewriter, loc, reductionOp, {slicedOperand, accumulator},
            resultType);
        accumulator = newOp->getResult(0);
      }
      rewriter.replaceOp(reductionOp, accumulator);
      return success();
    }

    // Vector result case.
    llvm::MapVector<
        SmallVector<int64_t>, Value,
        llvm::DenseMap<SmallVector<int64_t>, unsigned, OffsetMapInfo>>
        accCache;

    // Stride of the ratios, this gives us the offsets of sliceCount in a basis
    // of multiples of the targetShape.
    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalSize, *targetShape)) {
      SmallVector<Value> operands;
      SmallVector<int64_t> operandStrides(offsets.size(), 1);
      Value slicedOperand =
          rewriter.createOrFold<vector::ExtractStridedSliceOp>(
              loc, reductionOp.getSource(), offsets, *targetShape,
              operandStrides);
      operands.push_back(slicedOperand);
      SmallVector<int64_t> dstShape;
      SmallVector<int64_t> destOffset;
      for (size_t i : llvm::seq(size_t(0), targetShape->size())) {
        if (!reductionOp.isReducedDim(i)) {
          destOffset.push_back(offsets[i]);
          dstShape.push_back((*targetShape)[i]);
        }
      }
      Value acc;
      SmallVector<int64_t> accStrides(destOffset.size(), 1);
      // If a version of the accumulator has already been computed, use it
      // otherwise extract the first version from the original operand.
      auto *accIt = accCache.find(destOffset);
      if (accIt != accCache.end())
        acc = accIt->second;
      else
        acc = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
            loc, reductionOp.getAcc(), destOffset, dstShape, accStrides);
      operands.push_back(acc);
      auto targetType = VectorType::get(
          dstShape, reductionOp.getSourceVectorType().getElementType());
      Operation *newOp = cloneOpWithOperandsAndTypes(rewriter, loc, reductionOp,
                                                     operands, targetType);
      Value result = newOp->getResult(0);
      accCache[destOffset] = result;
    }
    // Assemble back the accumulator into a single vector.
    Value result = arith::ConstantOp::create(
        rewriter, loc, reductionOp.getDestType(),
        rewriter.getZeroAttr(reductionOp.getDestType()));
    for (const auto &it : accCache) {
      SmallVector<int64_t> dstStrides(it.first.size(), 1);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, it.second, result, it.first, dstStrides);
    }
    rewriter.replaceOp(reductionOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollElementwisePattern : public RewritePattern {
  UnrollElementwisePattern(MLIRContext *context,
                           const vector::UnrollVectorOptions &options,
                           PatternBenefit benefit = 1)
      : RewritePattern(MatchAnyOpTypeTag(), benefit, context),
        options(options) {}

  LogicalResult matchAndRewrite(Operation *op,
                                PatternRewriter &rewriter) const override {
    if (!OpTrait::hasElementwiseMappableTraits(op) || op->getNumResults() != 1)
      return failure();
    auto targetShape = getTargetShape(options, op);
    if (!targetShape)
      return failure();
    int64_t targetShapeRank = targetShape->size();
    auto dstVecType = cast<VectorType>(op->getResult(0).getType());
    SmallVector<int64_t> originalSize =
        *cast<VectorUnrollOpInterface>(op).getShapeForUnroll();
    int64_t originalShapeRank = originalSize.size();

    Location loc = op->getLoc();

    // Handle rank mismatch by adding leading unit dimensions to targetShape
    SmallVector<int64_t> adjustedTargetShape(originalShapeRank);
    int64_t rankDiff = originalShapeRank - targetShapeRank;
    std::fill(adjustedTargetShape.begin(),
              adjustedTargetShape.begin() + rankDiff, 1);
    std::copy(targetShape->begin(), targetShape->end(),
              adjustedTargetShape.begin() + rankDiff);

    int64_t adjustedTargetShapeRank = adjustedTargetShape.size();
    // Prepare the result vector.
    Value result = arith::ConstantOp::create(rewriter, loc, dstVecType,
                                             rewriter.getZeroAttr(dstVecType));
    SmallVector<int64_t> strides(adjustedTargetShapeRank, 1);
    VectorType unrolledVecType =
        VectorType::get(*targetShape, dstVecType.getElementType());

    // Create the unrolled computation.
    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalSize, adjustedTargetShape)) {
      SmallVector<Value> extractOperands;
      for (OpOperand &operand : op->getOpOperands()) {
        auto vecType = dyn_cast<VectorType>(operand.get().getType());
        if (!vecType) {
          extractOperands.push_back(operand.get());
          continue;
        }
        Value extracted = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
            loc, operand.get(), offsets, adjustedTargetShape, strides);

        // Reshape to remove leading unit dims if needed
        if (adjustedTargetShapeRank > targetShapeRank) {
          extracted = rewriter.createOrFold<vector::ShapeCastOp>(
              loc, VectorType::get(*targetShape, vecType.getElementType()),
              extracted);
        }
        extractOperands.push_back(extracted);
      }

      Operation *newOp = cloneOpWithOperandsAndTypes(
          rewriter, loc, op, extractOperands, unrolledVecType);

      Value computeResult = newOp->getResult(0);

      // Use strides sized to targetShape for proper insertion
      SmallVector<int64_t> insertStrides =
          (adjustedTargetShapeRank > targetShapeRank)
              ? SmallVector<int64_t>(targetShapeRank, 1)
              : strides;

      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, computeResult, result, offsets, insertStrides);
    }
    rewriter.replaceOp(op, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollReductionPattern : public OpRewritePattern<vector::ReductionOp> {
  UnrollReductionPattern(MLIRContext *context,
                         const vector::UnrollVectorOptions &options,
                         PatternBenefit benefit = 1)
      : OpRewritePattern<vector::ReductionOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::ReductionOp reductionOp,
                                PatternRewriter &rewriter) const override {
    std::optional<SmallVector<int64_t>> targetShape =
        getTargetShape(options, reductionOp);
    if (!targetShape)
      return failure();
    SmallVector<int64_t> originalSize = *reductionOp.getShapeForUnroll();

    // Create unrolled vector reduction.
    Location loc = reductionOp.getLoc();
    Value accumulator = nullptr;
    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalSize, *targetShape)) {
      SmallVector<int64_t> strides(offsets.size(), 1);
      Value slicedOperand =
          rewriter.createOrFold<vector::ExtractStridedSliceOp>(
              loc, reductionOp.getVector(), offsets, *targetShape, strides);
      Operation *newOp = cloneOpWithOperandsAndTypes(
          rewriter, loc, reductionOp, slicedOperand, reductionOp.getType());
      Value result = newOp->getResult(0);

      if (!accumulator) {
        // This is the first reduction.
        accumulator = result;
      } else {
        // On subsequent reduction, combine with the accumulator.
        accumulator = makeArithReduction(rewriter, loc, reductionOp.getKind(),
                                         accumulator, result);
      }
    }

    rewriter.replaceOp(reductionOp, accumulator);
    return success();
  }

private:
  const vector::UnrollVectorOptions options;
};

struct UnrollTransposePattern : public OpRewritePattern<vector::TransposeOp> {
  UnrollTransposePattern(MLIRContext *context,
                         const vector::UnrollVectorOptions &options,
                         PatternBenefit benefit = 1)
      : OpRewritePattern<vector::TransposeOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::TransposeOp transposeOp,
                                PatternRewriter &rewriter) const override {
    if (transposeOp.getResultVectorType().getRank() == 0)
      return failure();
    auto targetShape = getTargetShape(options, transposeOp);
    if (!targetShape)
      return failure();
    auto originalVectorType = transposeOp.getResultVectorType();
    SmallVector<int64_t> strides(targetShape->size(), 1);
    Location loc = transposeOp.getLoc();
    ArrayRef<int64_t> originalSize = originalVectorType.getShape();

    // Prepare the result vector;
    Value result =
        arith::ConstantOp::create(rewriter, loc, originalVectorType,
                                  rewriter.getZeroAttr(originalVectorType));
    ArrayRef<int64_t> permutation = transposeOp.getPermutation();

    // Unroll the computation.
    for (SmallVector<int64_t> elementOffsets :
         StaticTileOffsetRange(originalSize, *targetShape)) {
      SmallVector<int64_t> permutedOffsets(elementOffsets.size());
      SmallVector<int64_t> permutedShape(elementOffsets.size());
      // Compute the source offsets and shape.
      for (auto indices : llvm::enumerate(permutation)) {
        permutedOffsets[indices.value()] = elementOffsets[indices.index()];
        permutedShape[indices.value()] = (*targetShape)[indices.index()];
      }
      Value slicedOperand =
          rewriter.createOrFold<vector::ExtractStridedSliceOp>(
              loc, transposeOp.getVector(), permutedOffsets, permutedShape,
              strides);
      Value transposedSlice = rewriter.createOrFold<vector::TransposeOp>(
          loc, slicedOperand, permutation);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, transposedSlice, result, elementOffsets, strides);
    }
    rewriter.replaceOp(transposeOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollGatherPattern : public OpRewritePattern<vector::GatherOp> {
  UnrollGatherPattern(MLIRContext *context,
                      const vector::UnrollVectorOptions &options,
                      PatternBenefit benefit = 1)
      : OpRewritePattern<vector::GatherOp>(context, benefit), options(options) {
  }

  LogicalResult matchAndRewrite(vector::GatherOp gatherOp,
                                PatternRewriter &rewriter) const override {
    VectorType sourceVectorType = gatherOp.getVectorType();
    if (sourceVectorType.getRank() == 0)
      return failure();
    auto targetShape = getTargetShape(options, gatherOp);
    if (!targetShape)
      return failure();
    SmallVector<int64_t> strides(targetShape->size(), 1);
    Location loc = gatherOp.getLoc();
    ArrayRef<int64_t> originalSize = gatherOp.getVectorType().getShape();

    // Prepare the result vector;
    Value result =
        arith::ConstantOp::create(rewriter, loc, sourceVectorType,
                                  rewriter.getZeroAttr(sourceVectorType));
    auto targetType =
        VectorType::get(*targetShape, sourceVectorType.getElementType());

    SmallVector<int64_t> loopOrder =
        getUnrollOrder(originalSize.size(), gatherOp, options);
    for (SmallVector<int64_t> elementOffsets :
         StaticTileOffsetRange(originalSize, *targetShape, loopOrder)) {
      // To get the unrolled gather, extract the same slice based on the
      // decomposed shape from each of the index, mask, and pass-through
      // vectors.
      Value indexSubVec = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, gatherOp.getIndices(), elementOffsets, *targetShape, strides);
      Value maskSubVec = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, gatherOp.getMask(), elementOffsets, *targetShape, strides);
      Value passThruSubVec =
          rewriter.createOrFold<vector::ExtractStridedSliceOp>(
              loc, gatherOp.getPassThru(), elementOffsets, *targetShape,
              strides);
      auto slicedGather = vector::GatherOp::create(
          rewriter, loc, targetType, gatherOp.getBase(), gatherOp.getOffsets(),
          indexSubVec, maskSubVec, passThruSubVec);

      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, slicedGather, result, elementOffsets, strides);
    }
    rewriter.replaceOp(gatherOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollLoadPattern : public OpRewritePattern<vector::LoadOp> {
  UnrollLoadPattern(MLIRContext *context,
                    const vector::UnrollVectorOptions &options,
                    PatternBenefit benefit = 1)
      : OpRewritePattern<vector::LoadOp>(context, benefit), options(options) {}

  LogicalResult matchAndRewrite(vector::LoadOp loadOp,
                                PatternRewriter &rewriter) const override {
    VectorType vecType = loadOp.getVectorType();

    auto targetShape = getTargetShape(options, loadOp);
    if (!targetShape)
      return failure();

    Location loc = loadOp.getLoc();
    ArrayRef<int64_t> originalShape = vecType.getShape();
    SmallVector<int64_t> strides(targetShape->size(), 1);

    Value result = arith::ConstantOp::create(rewriter, loc, vecType,
                                             rewriter.getZeroAttr(vecType));

    SmallVector<int64_t> loopOrder =
        getUnrollOrder(originalShape.size(), loadOp, options);

    auto targetVecType =
        VectorType::get(*targetShape, vecType.getElementType());

    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalShape, *targetShape, loopOrder)) {
      SmallVector<Value> indices =
          sliceLoadStoreIndices(rewriter, loc, loadOp.getIndices(), offsets);
      Value slicedLoad = vector::LoadOp::create(rewriter, loc, targetVecType,
                                                loadOp.getBase(), indices);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, slicedLoad, result, offsets, strides);
    }
    rewriter.replaceOp(loadOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollStorePattern : public OpRewritePattern<vector::StoreOp> {
  UnrollStorePattern(MLIRContext *context,
                     const vector::UnrollVectorOptions &options,
                     PatternBenefit benefit = 1)
      : OpRewritePattern<vector::StoreOp>(context, benefit), options(options) {}

  LogicalResult matchAndRewrite(vector::StoreOp storeOp,
                                PatternRewriter &rewriter) const override {
    VectorType vecType = storeOp.getVectorType();

    auto targetShape = getTargetShape(options, storeOp);
    if (!targetShape)
      return failure();

    Location loc = storeOp.getLoc();
    ArrayRef<int64_t> originalShape = vecType.getShape();
    SmallVector<int64_t> strides(targetShape->size(), 1);

    Value base = storeOp.getBase();
    Value vector = storeOp.getValueToStore();

    SmallVector<int64_t> loopOrder =
        getUnrollOrder(originalShape.size(), storeOp, options);

    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalShape, *targetShape, loopOrder)) {
      SmallVector<Value> indices =
          sliceLoadStoreIndices(rewriter, loc, storeOp.getIndices(), offsets);
      Value slice = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, vector, offsets, *targetShape, strides);
      vector::StoreOp::create(rewriter, loc, slice, base, indices);
    }
    rewriter.eraseOp(storeOp);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

struct UnrollBroadcastPattern : public OpRewritePattern<vector::BroadcastOp> {
  UnrollBroadcastPattern(MLIRContext *context,
                         const vector::UnrollVectorOptions &options,
                         PatternBenefit benefit = 1)
      : OpRewritePattern<vector::BroadcastOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::BroadcastOp broadcastOp,
                                PatternRewriter &rewriter) const override {
    auto targetShape = getTargetShape(options, broadcastOp);
    if (!targetShape)
      return failure();

    Location loc = broadcastOp.getLoc();
    VectorType srcType = dyn_cast<VectorType>(broadcastOp.getSourceType());
    VectorType resType = broadcastOp.getResultVectorType();
    VectorType targetType =
        resType.cloneWith(*targetShape, resType.getElementType());
    Value result = arith::ConstantOp::create(rewriter, loc, resType,
                                             rewriter.getZeroAttr(resType));

    SmallVector<int64_t> originalShape = *broadcastOp.getShapeForUnroll();
    SmallVector<int64_t> strides(originalShape.size(), 1);

    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalShape, *targetShape)) {
      Value newSrc;
      if (!srcType) {
        // Scalar to vector broadcast.
        newSrc = broadcastOp.getSource();
      } else {
        // Vector to vector broadcast.
        int64_t rank = srcType.getRank();
        SmallVector<int64_t> srcOffsets(offsets.end() - rank, offsets.end());
        SmallVector<int64_t> srcShape(targetShape->end() - rank,
                                      targetShape->end());
        SmallVector<int64_t> srcStrides(strides.end() - rank, strides.end());
        // adjust the offset and shape for src if the corresponding dim is 1.
        for (int64_t i = 0; i < rank; ++i) {
          if (srcType.getDimSize(i) == 1) {
            srcOffsets[i] = 0;
            srcShape[i] = 1;
          }
        }
        newSrc = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
            loc, broadcastOp.getSource(), srcOffsets, srcShape, srcStrides);
      }

      Operation *newOp = cloneOpWithOperandsAndTypes(rewriter, loc, broadcastOp,
                                                     newSrc, targetType);

      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, newOp->getResult(0), result, offsets, strides);
    }

    rewriter.replaceOp(broadcastOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

/// Unrolls 2 or more dimensional `vector.to_elements` ops by unrolling the
/// outermost dimension of the operand. For example:
///
/// ```
/// %0:4 = vector.to_elements %v : vector<2x2xf32>
///
/// ==>
///
/// %v0 = vector.extract %v[0] : vector<2x2xf32> from vector<2x2x2xf32>
/// %v1 = vector.extract %v[1] : vector<2x2xf32> from vector<2x2x2xf32>
/// %0:4 = vector.to_elements %v0 : vector<2x2xf32>
/// %1:4 = vector.to_elements %v1 : vector<2x2xf32>
/// ```
///
/// When this pattern is applied until a fixed-point is reached,
/// this will produce a sequence of 1-d from_elements
/// ops.
struct UnrollToElements final : public OpRewritePattern<vector::ToElementsOp> {
  UnrollToElements(MLIRContext *context,
                   const vector::UnrollVectorOptions &options,
                   PatternBenefit benefit = 1)
      : OpRewritePattern<vector::ToElementsOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::ToElementsOp op,
                                PatternRewriter &rewriter) const override {

    TypedValue<VectorType> source = op.getSource();
    FailureOr<SmallVector<Value>> result =
        vector::unrollVectorValue(source, rewriter);
    if (failed(result)) {
      return failure();
    }
    SmallVector<Value> vectors = *result;

    SmallVector<Value> results;
    for (Value vector : vectors) {
      auto subElements =
          vector::ToElementsOp::create(rewriter, op.getLoc(), vector);
      llvm::append_range(results, subElements.getResults());
    }
    rewriter.replaceOp(op, results);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

/// This pattern unrolls `vector.step` operations according to the provided
/// target unroll shape. It decomposes a large step vector into smaller step
/// vectors (segments) and assembles the result by inserting each computed
/// segment into the appropriate offset of the original vector.
///
/// The pattern does not support scalable vectors and will fail to match them.
///
/// For each segment, it adds the base step vector and the segment's offset,
/// then inserts the result into the output vector at the corresponding
/// position.
///
/// Example:
///   Given a step operation:
///     %0 = vector.step : vector<8xindex>
///
///   and a target unroll shape of <4>, the pattern produces:
///
///     %base = vector.step : vector<4xindex>
///     %zero = arith.constant dense<0> : vector<8xindex>
///     %result0 = vector.insert_strided_slice %base, %zero
///       {offsets = [0], strides = [1]} : vector<4xindex> into vector<8xindex>
///     %offset = arith.constant dense<4> : vector<4xindex>
///     %segment1 = arith.addi %base, %offset : vector<4xindex>
///     %result1 = vector.insert_strided_slice %segment1, %result0
///       {offsets = [4], strides = [1]} : vector<4xindex> into vector<8xindex>
///
struct UnrollStepPattern : public OpRewritePattern<vector::StepOp> {
  UnrollStepPattern(MLIRContext *context,
                    const vector::UnrollVectorOptions &options,
                    PatternBenefit benefit = 1)
      : OpRewritePattern<vector::StepOp>(context, benefit), options(options) {}

  LogicalResult matchAndRewrite(vector::StepOp stepOp,
                                PatternRewriter &rewriter) const override {
    std::optional<SmallVector<int64_t>> targetShape =
        getTargetShape(options, stepOp);
    if (!targetShape)
      return failure();

    VectorType vecType = stepOp.getType();
    if (vecType.isScalable()) {
      // Scalable vectors are not supported by this pattern.
      return failure();
    }
    int64_t originalSize = vecType.getShape()[0];
    Location loc = stepOp.getLoc();
    SmallVector<int64_t> strides(1, 1);

    Value result = arith::ConstantOp::create(rewriter, loc, vecType,
                                             rewriter.getZeroAttr(vecType));

    auto targetVecType =
        VectorType::get(*targetShape, vecType.getElementType());
    Value baseStep = vector::StepOp::create(rewriter, loc, targetVecType);
    for (const SmallVector<int64_t> &offsets :
         StaticTileOffsetRange({originalSize}, *targetShape)) {
      Value bcastOffset = arith::ConstantOp::create(
          rewriter, loc, targetVecType,
          DenseElementsAttr::get(
              targetVecType,
              IntegerAttr::get(targetVecType.getElementType(), offsets[0])));
      Value tileStep =
          arith::AddIOp::create(rewriter, loc, baseStep, bcastOffset);

      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, tileStep, result, offsets, strides);
    }
    rewriter.replaceOp(stepOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

/// Unrolls 2 or more dimensional `vector.from_elements` ops by unrolling the
/// outermost dimension. For example:
/// ```
/// %v = vector.from_elements %e0, %e1, %e2, %e3, %e4, %e5 : vector<2x3xf32>
///
/// ==>
///
/// %0   = ub.poison : vector<2x3xf32>
/// %v0  = vector.from_elements %e0, %e1, %e2 : vector<3xf32>
/// %1   = vector.insert %v0, %0 [0] : vector<3xf32> into vector<2x3xf32>
/// %v1  = vector.from_elements %e3, %e4, %e5 : vector<3xf32>
/// %v   = vector.insert %v1, %1 [1] : vector<3xf32> into vector<2x3xf32>
/// ```
///
/// When this pattern is applied until a fixed-point is reached,
/// this will produce a sequence of 1-d from_elements
/// ops.
struct UnrollFromElements : OpRewritePattern<vector::FromElementsOp> {
  UnrollFromElements(MLIRContext *context,
                     const vector::UnrollVectorOptions &options,
                     PatternBenefit benefit = 1)
      : OpRewritePattern<vector::FromElementsOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::FromElementsOp op,
                                PatternRewriter &rewriter) const override {
    ValueRange allElements = op.getElements();

    auto unrollFromElementsFn = [&](PatternRewriter &rewriter, Location loc,
                                    VectorType subTy, int64_t index) {
      size_t subTyNumElements = subTy.getNumElements();
      assert((index + 1) * subTyNumElements <= allElements.size() &&
             "out of bounds");
      ValueRange subElements =
          allElements.slice(index * subTyNumElements, subTyNumElements);
      return vector::FromElementsOp::create(rewriter, loc, subTy, subElements);
    };

    return unrollVectorOp(op, rewriter, unrollFromElementsFn);
  }

private:
  vector::UnrollVectorOptions options;
};

/// This pattern unrolls `vector.create_mask` operations into smaller mask
/// operations based on the target unroll shape. Each unrolled slice computes
/// its local mask size in each dimension (d) as:
/// min(max(originalMaskSize[d] - offset[d], 0), unrolledDimSize[d]).
/// Example:
///   Given a create_mask operation:
///     %0 = vector.create_mask %c6, %c10 : vector<8x16xi1>  // mask first 6x10
///     elements
///
///   and a target unroll shape of <4x8>, the pattern produces:
///
///     %false = arith.constant dense<false> : vector<8x16xi1>
///
///     Slice [0,0]:
///     mask size = min(max(6-0, 0), 4) x min(max(10-0, 0), 8) = 4x8
///     %mask00 = vector.create_mask %c4, %c8 : vector<4x8xi1>
///     %r0 = vector.insert_strided_slice %mask00, %false [0, 0], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
///     Slice [0,8]:
///     mask size = min(max(6-0, 0), 4) x min(max(10-8, 0), 8) = 4x2
///     %mask01 = vector.create_mask %c4, %c2 : vector<4x8xi1>
///     %r1 = vector.insert_strided_slice %mask01, %r0 [0, 8], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
///     Slice [4,0]:
///     mask size = min(max(6-4, 0), 4) x min(max(10-0, 0), 8) = 2x8
///     %mask10 = vector.create_mask %c2, %c8 : vector<4x8xi1>
///     %r2 = vector.insert_strided_slice %mask10, %r1 [4, 0], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
///     Slice [4,8]:
///     mask size = min(max(6-4, 0), 4) x min(max(10-8, 0), 8) = 2x2
///     %mask11 = vector.create_mask %c2, %c2 : vector<4x8xi1>
///     %result = vector.insert_strided_slice %mask11, %r2 [4, 8], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
struct UnrollCreateMaskPattern : public OpRewritePattern<vector::CreateMaskOp> {
  UnrollCreateMaskPattern(MLIRContext *context,
                          const vector::UnrollVectorOptions &options,
                          PatternBenefit benefit = 1)
      : OpRewritePattern<vector::CreateMaskOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::CreateMaskOp createMaskOp,
                                PatternRewriter &rewriter) const override {
    auto targetShape = getTargetShape(options, createMaskOp);
    if (!targetShape)
      return failure();

    VectorType resultType = createMaskOp.getVectorType();
    SmallVector<int64_t> originalSize = *createMaskOp.getShapeForUnroll();
    Location loc = createMaskOp.getLoc();

    Value result = arith::ConstantOp::create(rewriter, loc, resultType,
                                             rewriter.getZeroAttr(resultType));
    VectorType targetVectorType =
        VectorType::get(*targetShape, rewriter.getI1Type());
    SmallVector<int64_t> strides(targetShape->size(), 1);

    // In each dimension (d), each unrolled vector computes its mask size as:
    // min(max(originalMaskOperands[d] - offset[d], 0), unrolledDimSize[d]).
    for (SmallVector<int64_t> offsets :
         StaticTileOffsetRange(originalSize, *targetShape)) {
      SmallVector<Value> unrolledOperands;

      for (auto [i, originalMaskOperand] :
           llvm::enumerate(createMaskOp.getOperands())) {
        Value offsetVal =
            arith::ConstantIndexOp::create(rewriter, loc, offsets[i]);
        Value adjustedMaskSize = rewriter.createOrFold<arith::SubIOp>(
            loc, originalMaskOperand, offsetVal);
        Value zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
        Value unrolledDimSize =
            arith::ConstantIndexOp::create(rewriter, loc, (*targetShape)[i]);
        Value nonNegative =
            rewriter.createOrFold<arith::MaxSIOp>(loc, adjustedMaskSize, zero);
        Value unrolledOperand = rewriter.createOrFold<arith::MinSIOp>(
            loc, nonNegative, unrolledDimSize);
        unrolledOperands.push_back(unrolledOperand);
      }

      auto unrolledMask = rewriter.createOrFold<vector::CreateMaskOp>(
          loc, targetVectorType, unrolledOperands);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, unrolledMask, result, offsets, strides);
    }
    rewriter.replaceOp(createMaskOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

/// This pattern unrolls `vector.constant_mask` operations into smaller mask
/// operations based on the target unroll shape. Each unrolled slice computes
/// whether its elements should be masked based on the original mask dimensions
/// and the slice's offset position.
///
/// Example:
///   Given a constant_mask operation:
///     %0 = vector.constant_mask [6, 10] : vector<8x16xi1>
///
///   and a target unroll shape of <4x8>, the pattern produces:
///
///     %false = arith.constant dense<false> : vector<8x16xi1>
///
///     Slice [0,0]: elements [0:4, 0:8] - fully within [6, 10] bounds
///     %mask00 = vector.constant_mask [4, 8] : vector<4x8xi1>
///     %r0 = vector.insert_strided_slice %mask00, %false [0, 0], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
///
///     Slice [0,8]: elements [0:4, 8:16] - partially within bounds
///     %mask01 = vector.constant_mask [4, 2] : vector<4x8xi1>
///     %r1 = vector.insert_strided_slice %mask01, %r0 [0, 8], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
///
///     Slice [4,0]: elements [4:8, 0:8] - partially within bounds
///     %mask10 = vector.constant_mask [2, 8] : vector<4x8xi1>
///     %r2 = vector.insert_strided_slice %mask10, %r1 [4, 0], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
///
///     Slice [4,8]: elements [4:8, 8:16] - partially within bounds
///     %mask11 = vector.constant_mask [2, 2] : vector<4x8xi1>
///     %result = vector.insert_strided_slice %mask11, %r2 [4, 8], [1, 1]
///       : vector<4x8xi1> into vector<8x16xi1>
struct UnrollConstantMaskPattern
    : public OpRewritePattern<vector::ConstantMaskOp> {
  UnrollConstantMaskPattern(MLIRContext *context,
                            const vector::UnrollVectorOptions &options,
                            PatternBenefit benefit = 1)
      : OpRewritePattern<vector::ConstantMaskOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::ConstantMaskOp constantMaskOp,
                                PatternRewriter &rewriter) const override {
    std::optional<SmallVector<int64_t>> targetShape =
        getTargetShape(options, constantMaskOp);
    if (!targetShape)
      return failure();

    VectorType resultType = constantMaskOp.getVectorType();
    SmallVector<int64_t> originalSize = *constantMaskOp.getShapeForUnroll();
    Location loc = constantMaskOp.getLoc();

    Value result = arith::ConstantOp::create(rewriter, loc, resultType,
                                             rewriter.getZeroAttr(resultType));
    VectorType targetVectorType =
        VectorType::get(*targetShape, rewriter.getI1Type());
    SmallVector<int64_t> strides(targetShape->size(), 1);

    // In each dimension (d), each unrolled vector computes its mask size as:
    // min(max(originalMaskDim[d] - offset[d], 0), unrolledDimSize[d]).
    for (const SmallVector<int64_t> &offsets :
         StaticTileOffsetRange(originalSize, *targetShape)) {
      SmallVector<int64_t> unrolledMaskDims;

      for (auto [i, originalMaskDim] :
           llvm::enumerate(constantMaskOp.getMaskDimSizes())) {
        // Calculate how many elements in this dimension should be masked
        // for this particular slice
        int64_t adjustedMaskSize =
            std::max(originalMaskDim - offsets[i], static_cast<int64_t>(0));
        int64_t unrolledMaskDim =
            std::min(adjustedMaskSize, static_cast<int64_t>((*targetShape)[i]));
        unrolledMaskDims.push_back(unrolledMaskDim);
      }

      auto unrolledMask = rewriter.createOrFold<vector::ConstantMaskOp>(
          loc, targetVectorType, unrolledMaskDims);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, unrolledMask, result, offsets, strides);
    }
    rewriter.replaceOp(constantMaskOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

/// Checks whether extractShape is a contiguous slice of shape.
/// For extractShape to be contiguous in shape:
/// 1) All but the leading dimension of extractShape and shape must match
/// exactly. 2) The total number of elements in shape must be evenly divisible
/// by
///    the total number of elements in extractShape.
/// Examples:
///   isContiguous([4, 4], [8, 4]) == true
///   isContiguous([2, 4], [8, 4]) == true
///   isContiguous([2, 2], [8, 4]) == false
/// Removes leading unit dimensions to handle cases like:
///   isContiguous([1, 16], [1, 32]) == true
static bool isContiguous(ArrayRef<int64_t> extractShape,
                         ArrayRef<int64_t> shape) {

  if (extractShape.empty() || shape.empty() ||
      extractShape.size() > shape.size())
    return false;

  while (extractShape.size() > 1 && extractShape.front() == 1)
    extractShape = extractShape.drop_front();

  while (shape.size() > 1 && shape.front() == 1) {
    shape = shape.drop_front();
  }

  size_t rankDiff = shape.size() - extractShape.size();
  if (!llvm::equal(extractShape.drop_front(), shape.drop_front(rankDiff + 1)))
    return false;

  int64_t extractElements = ShapedType::getNumElements(extractShape);
  int64_t shapeElements = ShapedType::getNumElements(shape);
  return shapeElements % extractElements == 0;
}

/// Determines what shape to use with `vector.extract_strided_slice` to extract
/// a contiguous memory region from a source vector. The extraction must be
/// contiguous and contain exactly the specified number of elements. If such an
/// extraction shape cannot be determined, returns std::nullopt.
/// EXAMPLE 1:
///   sourceShape = [16], targetElements = 8
///   Working right-to-left:
///   - Take min(8, 16) = 8 from only dim → extractShape = [8],
///     remaining = 8/8 = 1
///     Result: [8]
///
///  EXAMPLE 2:
///   sourceShape = [4, 4], targetElements = 8
///   Working right-to-left:
///   - Take min(8, 4) = 4 from last dim → extractShape = [4],
///     remaining = 8/4 = 2
///   - Take min(2, 4) = 2 from first dim → extractShape = [2, 4],
///     remaining = 2/2 = 1
///     Result: [2, 4]
static std::optional<SmallVector<int64_t>>
calculateSourceExtractShape(ArrayRef<int64_t> sourceShape,
                            int64_t targetElements) {
  SmallVector<int64_t> extractShape;
  int64_t remainingElements = targetElements;

  // Build extract shape from innermost dimension outward to ensure contiguity.
  for (int i = sourceShape.size() - 1; i >= 0 && remainingElements > 1; --i) {
    int64_t takeFromDim = std::min(remainingElements, sourceShape[i]);
    extractShape.insert(extractShape.begin(), takeFromDim);

    if (remainingElements % takeFromDim != 0)
      return std::nullopt; // Not evenly divisible.
    remainingElements /= takeFromDim;
  }

  // Fill remaining dimensions with 1.
  while (extractShape.size() < sourceShape.size())
    extractShape.insert(extractShape.begin(), 1);

  if (ShapedType::getNumElements(extractShape) != targetElements)
    return std::nullopt;

  return extractShape;
}

// Convert result offsets to source offsets via linear position.
static SmallVector<int64_t>
calculateSourceOffsets(ArrayRef<int64_t> resultOffsets,
                       ArrayRef<int64_t> sourceShape,
                       ArrayRef<int64_t> resultShape) {
  // Convert result offsets to linear position.
  int64_t linearIndex = linearize(resultOffsets, computeStrides(resultShape));
  // Convert linear position to source offsets.
  return delinearize(linearIndex, computeStrides(sourceShape));
}

/// A maximal aligned range of source dims [srcBegin, srcEnd) and result dims
/// [resBegin, resEnd) of a `vector.shape_cast` that hold equal element counts.
struct ShapeCastReassociationGroup {
  int64_t srcBegin, srcEnd;
  int64_t resBegin, resEnd;
};

/// Splits a shape_cast from `sourceShape` to `resultShape` into reassociation
/// groups (trailing unit dims absorbed). Returns nullopt if shapes misalign.
/// E.g. [8, 32, 32] -> [256, 32] ==> {[0,2)->[0,1)}, {[2,3)->[1,2)}
static std::optional<SmallVector<ShapeCastReassociationGroup>>
computeShapeCastGroups(ArrayRef<int64_t> sourceShape,
                       ArrayRef<int64_t> resultShape) {
  SmallVector<ShapeCastReassociationGroup> groups;
  int64_t si = 0, ri = 0;
  int64_t srcRank = sourceShape.size(), resRank = resultShape.size();
  while (si < srcRank && ri < resRank) {
    int64_t srcBegin = si, resBegin = ri;
    int64_t srcProd = sourceShape[si++];
    int64_t resProd = resultShape[ri++];
    // Grow the smaller side until both groups span the same element count.
    while (srcProd != resProd) {
      if (srcProd < resProd) {
        if (si >= srcRank)
          return std::nullopt;
        srcProd *= sourceShape[si++];
      } else {
        if (ri >= resRank)
          return std::nullopt;
        resProd *= resultShape[ri++];
      }
    }
    // Absorb trailing unit dimensions into the current group.
    while (si < srcRank && sourceShape[si] == 1)
      ++si;
    while (ri < resRank && resultShape[ri] == 1)
      ++ri;
    groups.push_back({srcBegin, si, resBegin, ri});
  }
  if (si != srcRank || ri != resRank)
    return std::nullopt;
  return groups;
}

/// This pattern unrolls `vector.shape_cast` operations according to the
/// provided target unroll shape. It unrolls a large shape cast into smaller
/// shape casts by extracting contiguous slices from the source vector, casting
/// each slice to the target shape, and assembling the result by inserting each
/// computed segment into the appropriate offset of the result vector.
///
/// The target tile need only be contiguous within each reassociation group of
/// the cast (not in the whole result vector), so that each extracted slice
/// remains a valid vector. The unrolling proceeds as:
/// vector.extract_strided_slice -> vector.shape_cast (on the slice) ->
/// vector.insert_strided_slice.
///
/// NOTE: This replaces a NOP `vector.shape_cast` with strided slices. Per-group
/// contiguity keeps those slices contiguous, so they are expected to lower to a
/// NOP too. Targets where strided slices do not lower to a NOP should not use
/// this pattern, or should pick a tile that avoids introducing such slices.
///
/// Example (single group):
///   Given a shape cast operation:
///     %0 = vector.shape_cast %src : vector<8x2xf32> to vector<4x4xf32>
///
///   and a target unroll shape of <2x4>, the pattern produces:
///
///     %zero = arith.constant dense<0.0> : vector<4x4xf32>
///     %s0 = vector.extract_strided_slice %src [0, 0], [4, 2], [1, 1]
///       : vector<8x2xf32> to vector<4x2xf32>
///     %sc0 = vector.shape_cast %s0 : vector<4x2xf32> to vector<2x4xf32>
///     %i0 = vector.insert_strided_slice %sc0, %zero [0, 0], [1, 1]
///       : vector<2x4xf32> into vector<4x4xf32>
///     %s1 = vector.extract_strided_slice %src [4, 0], [4, 2], [1, 1]
///       : vector<8x2xf32> to vector<4x2xf32>
///     %sc1 = vector.shape_cast %s1 : vector<4x2xf32> to vector<2x4xf32>
///     %i1 = vector.insert_strided_slice %sc1, %i0 [2, 0], [1, 1]
///       : vector<2x4xf32> into vector<4x4xf32>
///
/// Example (multiple groups): with target tile <8x1x4>, the tile is strided in
/// the result <8x1x32> but contiguous per group (8|32 -> 8x1|32), so the
/// matching strided box <8x4> is extracted from the source:
///     %0 = vector.shape_cast %src : vector<8x32xf32> to vector<8x1x32xf32>
///
///     %s0 = vector.extract_strided_slice %src [0, 0], [8, 4], [1, 1]
///       : vector<8x32xf32> to vector<8x4xf32>
///     %sc0 = vector.shape_cast %s0 : vector<8x4xf32> to vector<8x1x4xf32>
///     %i0 = vector.insert_strided_slice %sc0, %zero [0, 0, 0], [1, 1, 1]
///       : vector<8x1x4xf32> into vector<8x1x32xf32>
///     // ... repeat for the remaining slices.
///
struct UnrollShapeCastPattern : public OpRewritePattern<vector::ShapeCastOp> {
  UnrollShapeCastPattern(MLIRContext *context,
                         const vector::UnrollVectorOptions &options,
                         PatternBenefit benefit = 1)
      : OpRewritePattern<vector::ShapeCastOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::ShapeCastOp shapeCastOp,
                                PatternRewriter &rewriter) const override {
    std::optional<SmallVector<int64_t>> targetShape =
        getTargetShape(options, shapeCastOp);
    if (!targetShape)
      return failure();

    VectorType sourceType = shapeCastOp.getSourceVectorType();
    VectorType resultType = shapeCastOp.getResultVectorType();
    ArrayRef<int64_t> sourceShape = sourceType.getShape();
    ArrayRef<int64_t> resultShape = resultType.getShape();

    // The cast factors into reassociation groups; the target tile only needs to
    // be contiguous within each group, not in the whole result vector.
    std::optional<SmallVector<ShapeCastReassociationGroup>> groups =
        computeShapeCastGroups(sourceShape, resultShape);
    if (!groups)
      return rewriter.notifyMatchFailure(
          shapeCastOp, "cannot align source and result reassociation groups");

    // The tile is right-aligned against the result; left-pad with 1s so it can
    // be indexed per group.
    SmallVector<int64_t> paddedTarget(resultShape.size(), 1);
    llvm::copy(*targetShape,
               paddedTarget.end() - static_cast<int64_t>(targetShape->size()));

    // Validate per-group contiguity and build the source extract shape.
    SmallVector<int64_t> extractShapeStorage;
    for (const ShapeCastReassociationGroup &g : *groups) {
      ArrayRef<int64_t> resSub =
          resultShape.slice(g.resBegin, g.resEnd - g.resBegin);
      ArrayRef<int64_t> tgtSub = ArrayRef<int64_t>(paddedTarget)
                                     .slice(g.resBegin, g.resEnd - g.resBegin);
      if (!isContiguous(tgtSub, resSub))
        return rewriter.notifyMatchFailure(
            shapeCastOp, "target shape is not contiguous within a "
                         "reassociation group of the result vector shape");

      ArrayRef<int64_t> srcSub =
          sourceShape.slice(g.srcBegin, g.srcEnd - g.srcBegin);
      int64_t groupTargetElements = ShapedType::getNumElements(tgtSub);
      std::optional<SmallVector<int64_t>> groupExtract =
          calculateSourceExtractShape(srcSub, groupTargetElements);
      if (!groupExtract)
        return rewriter.notifyMatchFailure(
            shapeCastOp, "cannot extract the target number of elements "
                         "contiguously from a source reassociation group");
      extractShapeStorage.append(groupExtract->begin(), groupExtract->end());
    }
    ArrayRef<int64_t> extractShape = extractShapeStorage;

    Location loc = shapeCastOp.getLoc();

    // Create result vector initialized to zero.
    Value result = arith::ConstantOp::create(rewriter, loc, resultType,
                                             rewriter.getZeroAttr(resultType));

    VectorType targetType =
        VectorType::get(*targetShape, sourceType.getElementType());

    SmallVector<int64_t> extractStrides(extractShape.size(), 1);
    SmallVector<int64_t> insertStrides(targetShape->size(), 1);

    for (SmallVector<int64_t> resultOffsets :
         StaticTileOffsetRange(resultShape, *targetShape)) {
      SmallVector<int64_t> sourceOffsets =
          calculateSourceOffsets(resultOffsets, sourceShape, resultShape);
      Value sourceChunk = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, shapeCastOp.getSource(), sourceOffsets, extractShape,
          extractStrides);
      Value targetChunk = rewriter.createOrFold<vector::ShapeCastOp>(
          loc, targetType, sourceChunk);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, targetChunk, result, resultOffsets, insertStrides);
    }

    rewriter.replaceOp(shapeCastOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

// Unroll vector::BitCastOp into smaller slice-based bitcast operations.
// Decomposes the result vector into target shape chunks and bitcasts
// corresponding source slices, accounting for element bitwidth ratios.
/// Example:
///   Given a bitcast Op:
///
///     vector.bitcast %src : vector<4x8xf32>
///
///   and a target unroll shape of <2x4>, the pattern produces:
///
///     %slice_0 = vector.extract_strided_slice %lhs[0, 0] : vector<2x4xf32>
///     %slice_0 = vector.bitcast %slice_0 : vector<2x4xf32>
///     %result = vector.insert_strided_slice %slice_0, %init[0, 0]
///     // ... repeat for remaining slices
struct UnrollBitCastPattern : public OpRewritePattern<vector::BitCastOp> {
  UnrollBitCastPattern(MLIRContext *context,
                       const vector::UnrollVectorOptions &options,
                       PatternBenefit benefit = 1)
      : OpRewritePattern<vector::BitCastOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::BitCastOp bitCastOp,
                                PatternRewriter &rewriter) const override {
    auto targetShape = getTargetShape(options, bitCastOp);
    if (!targetShape)
      return rewriter.notifyMatchFailure(bitCastOp,
                                         "failed to get target shape");

    VectorType sourceType = bitCastOp.getSourceVectorType();
    VectorType resultType = bitCastOp.getResultVectorType();
    ArrayRef<int64_t> resultShape = resultType.getShape();
    Location loc = bitCastOp.getLoc();

    if (targetShape->size() != resultShape.size())
      return rewriter.notifyMatchFailure(
          bitCastOp, "target shape rank must match result rank");

    unsigned sourceElementBits = sourceType.getElementTypeBitWidth();
    unsigned resultElementBits = resultType.getElementTypeBitWidth();

    SmallVector<int64_t> sourceSliceShape(targetShape->begin(),
                                          targetShape->end());
    int64_t lastDim = sourceSliceShape.size() - 1;

    sourceSliceShape[lastDim] =
        ((*targetShape)[lastDim] * resultElementBits) / sourceElementBits;

    Value result = arith::ConstantOp::create(rewriter, loc, resultType,
                                             rewriter.getZeroAttr(resultType));
    SmallVector<int64_t> resultStrides(targetShape->size(), 1);
    SmallVector<int64_t> sourceStrides(sourceSliceShape.size(), 1);

    VectorType targetType =
        VectorType::get(*targetShape, resultType.getElementType());

    for (SmallVector<int64_t> resultOffsets :
         StaticTileOffsetRange(resultShape, *targetShape)) {
      SmallVector<int64_t> sourceOffsets = resultOffsets;
      sourceOffsets[lastDim] =
          (resultOffsets[lastDim] * resultElementBits) / sourceElementBits;

      Value sourceSlice = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, bitCastOp.getSource(), sourceOffsets, sourceSliceShape,
          sourceStrides);
      Value bitcastSlice = rewriter.createOrFold<vector::BitCastOp>(
          loc, targetType, sourceSlice);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, bitcastSlice, result, resultOffsets, resultStrides);
    }

    rewriter.replaceOp(bitCastOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

/// Pattern to unroll vector.interleave into smaller slice-sized operations.
/// Decomposes a large interleave into slices by extracting slices from both
/// input vectors, interleaving them, and inserting back into the result.
///
/// Example:
///   Given an interleave Op:
///
///     vector.interleave %lhs, %rhs : vector<4x8xf32>
///
///   and a target unroll shape of <2x4>, the pattern produces:
///
///     %slice_lhs_0 = vector.extract_strided_slice %lhs[0, 0] : vector<2x2xf32>
///     %slice_rhs_0 = vector.extract_strided_slice %rhs[0, 0] : vector<2x2xf32>
///     %slice_0 = vector.interleave %slice_lhs_0, %slice_rhs_0
///       : vector<2x4xf32>
///     %result = vector.insert_strided_slice %slice_0, %init[0, 0]
///     // ... repeat for remaining slices
struct UnrollInterleavePattern : public OpRewritePattern<vector::InterleaveOp> {
  UnrollInterleavePattern(MLIRContext *context,
                          const vector::UnrollVectorOptions &options,
                          PatternBenefit benefit = 1)
      : OpRewritePattern<vector::InterleaveOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::InterleaveOp interleaveOp,
                                PatternRewriter &rewriter) const override {
    auto targetShape = getTargetShape(options, interleaveOp);
    if (!targetShape)
      return rewriter.notifyMatchFailure(interleaveOp,
                                         "failed to get target shape");

    VectorType resultType = interleaveOp.getResultVectorType();
    ArrayRef<int64_t> resultShape = resultType.getShape();
    Location loc = interleaveOp.getLoc();

    if (targetShape->size() != resultShape.size())
      return rewriter.notifyMatchFailure(
          interleaveOp, "target shape rank must match result rank");

    SmallVector<int64_t> sourceSliceShape(targetShape->begin(),
                                          targetShape->end());
    int64_t lastDim = sourceSliceShape.size() - 1;
    sourceSliceShape[lastDim] = (*targetShape)[lastDim] / 2;

    Value result = arith::ConstantOp::create(rewriter, loc, resultType,
                                             rewriter.getZeroAttr(resultType));
    SmallVector<int64_t> resultStrides(targetShape->size(), 1);
    SmallVector<int64_t> sourceStrides(sourceSliceShape.size(), 1);

    VectorType targetType =
        VectorType::get(*targetShape, resultType.getElementType());

    for (SmallVector<int64_t> resultOffsets :
         StaticTileOffsetRange(resultShape, *targetShape)) {
      SmallVector<int64_t> sourceOffsets = resultOffsets;
      sourceOffsets[lastDim] = resultOffsets[lastDim] / 2;

      Value lhsSlice = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, interleaveOp.getLhs(), sourceOffsets, sourceSliceShape,
          sourceStrides);
      Value rhsSlice = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, interleaveOp.getRhs(), sourceOffsets, sourceSliceShape,
          sourceStrides);
      Value interleaveSlice = rewriter.createOrFold<vector::InterleaveOp>(
          loc, targetType, lhsSlice, rhsSlice);
      result = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, interleaveSlice, result, resultOffsets, resultStrides);
    }

    rewriter.replaceOp(interleaveOp, result);
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

/// Pattern to unroll vector.deinterleave into smaller slice-sized operations.
/// Decomposes a large deinterleave (which splits a vector into even/odd halves)
/// by extracting source slices, deinterleaving them, and inserting into two
/// result vectors.
///
/// Example:
///   Given a deinterleave Op:
///
///     vector.deinterleave %src : vector<4x8xf32>
///
///   and a target unroll shape of <2x4>, the pattern produces:
///
///   %slice_0 = vector.extract_strided_slice %src[0, 0] : vector<2x4xf32>
///   %slice_lhs_0, %slice_rhs_0 = vector.deinterleave %slice_0 :
///   vector<2x4xf32> %result1 = vector.insert_strided_slice %slice_lhs_0,
///   %init1[0, 0] %result2 = vector.insert_strided_slice %slice_rhs_0,
///   %init2[0, 0]
///   // ... repeat for remaining slices
struct UnrollDeinterleavePattern
    : public OpRewritePattern<vector::DeinterleaveOp> {
  UnrollDeinterleavePattern(MLIRContext *context,
                            const vector::UnrollVectorOptions &options,
                            PatternBenefit benefit = 1)
      : OpRewritePattern<vector::DeinterleaveOp>(context, benefit),
        options(options) {}

  LogicalResult matchAndRewrite(vector::DeinterleaveOp deinterleaveOp,
                                PatternRewriter &rewriter) const override {
    auto targetShape = getTargetShape(options, deinterleaveOp);
    if (!targetShape)
      return rewriter.notifyMatchFailure(deinterleaveOp,
                                         "failed to get target shape");

    VectorType resultType = deinterleaveOp.getResultVectorType();
    ArrayRef<int64_t> resultShape = resultType.getShape();
    Location loc = deinterleaveOp.getLoc();

    if (targetShape->size() != resultShape.size())
      return rewriter.notifyMatchFailure(
          deinterleaveOp, "target shape rank must match result rank");

    SmallVector<int64_t> sourceSliceShape(targetShape->begin(),
                                          targetShape->end());
    int64_t lastDim = sourceSliceShape.size() - 1;
    sourceSliceShape[lastDim] = (*targetShape)[lastDim] * 2;

    Value resultOdd = arith::ConstantOp::create(
        rewriter, loc, resultType, rewriter.getZeroAttr(resultType));
    Value resultEven = arith::ConstantOp::create(
        rewriter, loc, resultType, rewriter.getZeroAttr(resultType));
    SmallVector<int64_t> resultStrides(targetShape->size(), 1);
    SmallVector<int64_t> sourceStrides(sourceSliceShape.size(), 1);

    for (SmallVector<int64_t> resultOffsets :
         StaticTileOffsetRange(resultShape, *targetShape)) {
      SmallVector<int64_t> sourceOffsets = resultOffsets;
      sourceOffsets[lastDim] = resultOffsets[lastDim] * 2;

      Value sourceSlice = rewriter.createOrFold<vector::ExtractStridedSliceOp>(
          loc, deinterleaveOp.getSource(), sourceOffsets, sourceSliceShape,
          sourceStrides);

      auto deinterleaveSlice =
          vector::DeinterleaveOp::create(rewriter, loc, sourceSlice);

      resultOdd = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, deinterleaveSlice.getRes1(), resultOdd, resultOffsets,
          resultStrides);
      resultEven = rewriter.createOrFold<vector::InsertStridedSliceOp>(
          loc, deinterleaveSlice.getRes2(), resultEven, resultOffsets,
          resultStrides);
    }

    rewriter.replaceOp(deinterleaveOp, ValueRange{resultOdd, resultEven});
    return success();
  }

private:
  vector::UnrollVectorOptions options;
};

} // namespace

void mlir::vector::populateVectorUnrollPatterns(
    RewritePatternSet &patterns, const UnrollVectorOptions &options,
    PatternBenefit benefit) {
  patterns.add<UnrollTransferReadPattern, UnrollTransferWritePattern,
               UnrollContractionPattern, UnrollElementwisePattern,
               UnrollReductionPattern, UnrollMultiReductionPattern,
               UnrollTransposePattern, UnrollGatherPattern, UnrollLoadPattern,
               UnrollStorePattern, UnrollBroadcastPattern, UnrollFromElements,
               UnrollToElements, UnrollStepPattern, UnrollShapeCastPattern,
               UnrollCreateMaskPattern, UnrollConstantMaskPattern,
               UnrollBitCastPattern, UnrollInterleavePattern,
               UnrollDeinterleavePattern>(patterns.getContext(), options,
                                          benefit);
}

void mlir::vector::populateVectorToElementsUnrollPatterns(
    RewritePatternSet &patterns, PatternBenefit benefit) {
  patterns.add<UnrollToElements>(patterns.getContext(), UnrollVectorOptions(),
                                 benefit);
}

void mlir::vector::populateVectorFromElementsUnrollPatterns(
    RewritePatternSet &patterns, PatternBenefit benefit) {
  patterns.add<UnrollFromElements>(patterns.getContext(), UnrollVectorOptions(),
                                   benefit);
}
