//===- polly/ScheduleTreeTransform.cpp --------------------------*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// Make changes to isl's schedule tree data structure.
//
//===----------------------------------------------------------------------===//

#include "polly/ScheduleTreeTransform.h"
#include "polly/Support/GICHelper.h"
#include "polly/Support/ISLTools.h"
#include "polly/Support/ScopHelper.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/Sequence.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/Metadata.h"
#include "llvm/Transforms/Utils/UnrollLoop.h"

#define DEBUG_TYPE "polly-opt-isl"

using namespace polly;
using namespace llvm;

namespace {

/// Copy the band member attributes (coincidence, loop type, isolate ast loop
/// type) from one band to another.
static isl::schedule_node_band
applyBandMemberAttributes(isl::schedule_node_band Target, int TargetIdx,
                          const isl::schedule_node_band &Source,
                          int SourceIdx) {
  bool Coincident = Source.member_get_coincident(SourceIdx).release();
  Target = Target.member_set_coincident(TargetIdx, Coincident);

  isl_ast_loop_type LoopType =
      isl_schedule_node_band_member_get_ast_loop_type(Source.get(), SourceIdx);
  Target = isl::manage(isl_schedule_node_band_member_set_ast_loop_type(
                           Target.release(), TargetIdx, LoopType))
               .as<isl::schedule_node_band>();

  isl_ast_loop_type IsolateType =
      isl_schedule_node_band_member_get_isolate_ast_loop_type(Source.get(),
                                                              SourceIdx);
  Target = isl::manage(isl_schedule_node_band_member_set_isolate_ast_loop_type(
                           Target.release(), TargetIdx, IsolateType))
               .as<isl::schedule_node_band>();

  return Target;
}

/// Create a new band by copying members from another @p Band. @p IncludeCb
/// decides which band indices are copied to the result.
template <typename CbTy>
static isl::schedule rebuildBand(isl::schedule_node_band OldBand,
                                 isl::schedule Body, CbTy IncludeCb) {
  int NumBandDims = unsignedFromIslSize(OldBand.n_member());

  bool ExcludeAny = false;
  bool IncludeAny = false;
  for (auto OldIdx : seq<int>(0, NumBandDims)) {
    if (IncludeCb(OldIdx))
      IncludeAny = true;
    else
      ExcludeAny = true;
  }

  // Instead of creating a zero-member band, don't create a band at all.
  if (!IncludeAny)
    return Body;

  isl::multi_union_pw_aff PartialSched = OldBand.get_partial_schedule();
  isl::multi_union_pw_aff NewPartialSched;
  if (ExcludeAny) {
    // Select the included partial scatter functions.
    isl::union_pw_aff_list List = PartialSched.list();
    int NewIdx = 0;
    for (auto OldIdx : seq<int>(0, NumBandDims)) {
      if (IncludeCb(OldIdx))
        NewIdx += 1;
      else
        List = List.drop(NewIdx, 1);
    }
    isl::space ParamSpace = PartialSched.get_space().params();
    isl::space NewScatterSpace = ParamSpace.add_unnamed_tuple(NewIdx);
    NewPartialSched = isl::multi_union_pw_aff(NewScatterSpace, List);
  } else {
    // Just reuse original scatter function of copying all of them.
    NewPartialSched = PartialSched;
  }

  // Create the new band node.
  isl::schedule_node_band NewBand =
      Body.insert_partial_schedule(NewPartialSched)
          .get_root()
          .child(0)
          .as<isl::schedule_node_band>();

  // If OldBand was permutable, so is the new one, even if some dimensions are
  // missing.
  bool IsPermutable = OldBand.permutable().release();
  NewBand = NewBand.set_permutable(IsPermutable);

  // Reapply member attributes.
  int NewIdx = 0;
  for (auto OldIdx : seq<int>(0, NumBandDims)) {
    if (!IncludeCb(OldIdx))
      continue;
    NewBand =
        applyBandMemberAttributes(std::move(NewBand), NewIdx, OldBand, OldIdx);
    NewIdx += 1;
  }

  return NewBand.get_schedule();
}

/// Rewrite a schedule tree by reconstructing it bottom-up.
///
/// By default, the original schedule tree is reconstructed. To build a
/// different tree, redefine visitor methods in a derived class (CRTP).
///
/// Note that AST build options are not applied; Setting the isolate[] option
/// makes the schedule tree 'anchored' and cannot be modified afterwards. Hence,
/// AST build options must be set after the tree has been constructed.
template <typename Derived, typename... Args>
struct ScheduleTreeRewriter
    : RecursiveScheduleTreeVisitor<Derived, isl::schedule, Args...> {
  Derived &getDerived() { return *static_cast<Derived *>(this); }
  const Derived &getDerived() const {
    return *static_cast<const Derived *>(this);
  }

  isl::schedule visitDomain(isl::schedule_node_domain Node, Args... args) {
    // Every schedule_tree already has a domain node, no need to add one.
    return getDerived().visit(Node.first_child(), std::forward<Args>(args)...);
  }

  isl::schedule visitBand(isl::schedule_node_band Band, Args... args) {
    isl::schedule NewChild =
        getDerived().visit(Band.child(0), std::forward<Args>(args)...);
    return rebuildBand(Band, NewChild, [](int) { return true; });
  }

  isl::schedule visitSequence(isl::schedule_node_sequence Sequence,
                              Args... args) {
    int NumChildren = isl_schedule_node_n_children(Sequence.get());
    isl::schedule Result =
        getDerived().visit(Sequence.child(0), std::forward<Args>(args)...);
    for (int i = 1; i < NumChildren; i += 1)
      Result = Result.sequence(
          getDerived().visit(Sequence.child(i), std::forward<Args>(args)...));
    return Result;
  }

  isl::schedule visitSet(isl::schedule_node_set Set, Args... args) {
    int NumChildren = isl_schedule_node_n_children(Set.get());
    isl::schedule Result =
        getDerived().visit(Set.child(0), std::forward<Args>(args)...);
    for (int i = 1; i < NumChildren; i += 1)
      Result = isl::manage(
          isl_schedule_set(Result.release(),
                           getDerived()
                               .visit(Set.child(i), std::forward<Args>(args)...)
                               .release()));
    return Result;
  }

  isl::schedule visitLeaf(isl::schedule_node_leaf Leaf, Args... args) {
    return isl::schedule::from_domain(Leaf.get_domain());
  }

  isl::schedule visitMark(const isl::schedule_node &Mark, Args... args) {

    isl::id TheMark = Mark.as<isl::schedule_node_mark>().get_id();
    isl::schedule_node NewChild =
        getDerived()
            .visit(Mark.first_child(), std::forward<Args>(args)...)
            .get_root()
            .first_child();
    return NewChild.insert_mark(TheMark).get_schedule();
  }

  isl::schedule visitExtension(isl::schedule_node_extension Extension,
                               Args... args) {
    isl::union_map TheExtension =
        Extension.as<isl::schedule_node_extension>().get_extension();
    isl::schedule_node NewChild = getDerived()
                                      .visit(Extension.child(0), args...)
                                      .get_root()
                                      .first_child();
    isl::schedule_node NewExtension =
        isl::schedule_node::from_extension(TheExtension);
    return NewChild.graft_before(NewExtension).get_schedule();
  }

  isl::schedule visitFilter(isl::schedule_node_filter Filter, Args... args) {
    isl::union_set FilterDomain =
        Filter.as<isl::schedule_node_filter>().get_filter();
    isl::schedule NewSchedule =
        getDerived().visit(Filter.child(0), std::forward<Args>(args)...);
    return NewSchedule.intersect_domain(FilterDomain);
  }

  isl::schedule visitNode(isl::schedule_node Node, Args... args) {
    llvm_unreachable("Not implemented");
  }
};

/// Rewrite the schedule tree without any changes. Useful to copy a subtree into
/// a new schedule, discarding everything but.
struct IdentityRewriter : ScheduleTreeRewriter<IdentityRewriter> {};

/// Rewrite a schedule tree to an equivalent one without extension nodes.
///
/// Each visit method takes two additional arguments:
///
///  * The new domain the node, which is the inherited domain plus any domains
///    added by extension nodes.
///
///  * A map of extension domains of all children is returned; it is required by
///    band nodes to schedule the additional domains at the same position as the
///    extension node would.
///
struct ExtensionNodeRewriter final
    : ScheduleTreeRewriter<ExtensionNodeRewriter, const isl::union_set &,
                           isl::union_map &> {
  using BaseTy = ScheduleTreeRewriter<ExtensionNodeRewriter,
                                      const isl::union_set &, isl::union_map &>;
  BaseTy &getBase() { return *this; }
  const BaseTy &getBase() const { return *this; }

  isl::schedule visitSchedule(isl::schedule Schedule) {
    isl::union_map Extensions;
    isl::schedule Result =
        visit(Schedule.get_root(), Schedule.get_domain(), Extensions);
    assert(!Extensions.is_null() && Extensions.is_empty());
    return Result;
  }

  isl::schedule visitSequence(isl::schedule_node_sequence Sequence,
                              const isl::union_set &Domain,
                              isl::union_map &Extensions) {
    int NumChildren = isl_schedule_node_n_children(Sequence.get());
    isl::schedule NewNode = visit(Sequence.first_child(), Domain, Extensions);
    for (int i = 1; i < NumChildren; i += 1) {
      isl::schedule_node OldChild = Sequence.child(i);
      isl::union_map NewChildExtensions;
      isl::schedule NewChildNode = visit(OldChild, Domain, NewChildExtensions);
      NewNode = NewNode.sequence(NewChildNode);
      Extensions = Extensions.unite(NewChildExtensions);
    }
    return NewNode;
  }

  isl::schedule visitSet(isl::schedule_node_set Set,
                         const isl::union_set &Domain,
                         isl::union_map &Extensions) {
    int NumChildren = isl_schedule_node_n_children(Set.get());
    isl::schedule NewNode = visit(Set.first_child(), Domain, Extensions);
    for (int i = 1; i < NumChildren; i += 1) {
      isl::schedule_node OldChild = Set.child(i);
      isl::union_map NewChildExtensions;
      isl::schedule NewChildNode = visit(OldChild, Domain, NewChildExtensions);
      NewNode = isl::manage(
          isl_schedule_set(NewNode.release(), NewChildNode.release()));
      Extensions = Extensions.unite(NewChildExtensions);
    }
    return NewNode;
  }

  isl::schedule visitLeaf(isl::schedule_node_leaf Leaf,
                          const isl::union_set &Domain,
                          isl::union_map &Extensions) {
    Extensions = isl::union_map::empty(Leaf.ctx());
    return isl::schedule::from_domain(Domain);
  }

  isl::schedule visitBand(isl::schedule_node_band OldNode,
                          const isl::union_set &Domain,
                          isl::union_map &OuterExtensions) {
    isl::schedule_node OldChild = OldNode.first_child();
    isl::multi_union_pw_aff PartialSched =
        isl::manage(isl_schedule_node_band_get_partial_schedule(OldNode.get()));

    isl::union_map NewChildExtensions;
    isl::schedule NewChild = visit(OldChild, Domain, NewChildExtensions);

    // Add the extensions to the partial schedule.
    OuterExtensions = isl::union_map::empty(NewChildExtensions.ctx());
    isl::union_map NewPartialSchedMap = isl::union_map::from(PartialSched);
    unsigned BandDims = isl_schedule_node_band_n_member(OldNode.get());
    for (isl::map Ext : NewChildExtensions.get_map_list()) {
      unsigned ExtDims = unsignedFromIslSize(Ext.domain_tuple_dim());
      assert(ExtDims >= BandDims);
      unsigned OuterDims = ExtDims - BandDims;

      isl::map BandSched =
          Ext.project_out(isl::dim::in, 0, OuterDims).reverse();
      NewPartialSchedMap = NewPartialSchedMap.unite(BandSched);

      // There might be more outer bands that have to schedule the extensions.
      if (OuterDims > 0) {
        isl::map OuterSched =
            Ext.project_out(isl::dim::in, OuterDims, BandDims);
        OuterExtensions = OuterExtensions.unite(OuterSched);
      }
    }
    isl::multi_union_pw_aff NewPartialSchedAsAsMultiUnionPwAff =
        isl::multi_union_pw_aff::from_union_map(NewPartialSchedMap);
    isl::schedule_node NewNode =
        NewChild.insert_partial_schedule(NewPartialSchedAsAsMultiUnionPwAff)
            .get_root()
            .child(0);

    // Reapply permutability and coincidence attributes.
    NewNode = isl::manage(isl_schedule_node_band_set_permutable(
        NewNode.release(),
        isl_schedule_node_band_get_permutable(OldNode.get())));
    for (unsigned i = 0; i < BandDims; i += 1)
      NewNode = applyBandMemberAttributes(NewNode.as<isl::schedule_node_band>(),
                                          i, OldNode, i);

    return NewNode.get_schedule();
  }

  isl::schedule visitFilter(isl::schedule_node_filter Filter,
                            const isl::union_set &Domain,
                            isl::union_map &Extensions) {
    isl::union_set FilterDomain =
        Filter.as<isl::schedule_node_filter>().get_filter();
    isl::union_set NewDomain = Domain.intersect(FilterDomain);

    // A filter is added implicitly if necessary when joining schedule trees.
    return visit(Filter.first_child(), NewDomain, Extensions);
  }

  isl::schedule visitExtension(isl::schedule_node_extension Extension,
                               const isl::union_set &Domain,
                               isl::union_map &Extensions) {
    isl::union_map ExtDomain =
        Extension.as<isl::schedule_node_extension>().get_extension();
    isl::union_set NewDomain = Domain.unite(ExtDomain.range());
    isl::union_map ChildExtensions;
    isl::schedule NewChild =
        visit(Extension.first_child(), NewDomain, ChildExtensions);
    Extensions = ChildExtensions.unite(ExtDomain);
    return NewChild;
  }
};

/// Collect all AST build options in any schedule tree band.
///
/// ScheduleTreeRewriter cannot apply the schedule tree options. This class
/// collects these options to apply them later.
struct CollectASTBuildOptions final
    : RecursiveScheduleTreeVisitor<CollectASTBuildOptions> {
  using BaseTy = RecursiveScheduleTreeVisitor<CollectASTBuildOptions>;
  BaseTy &getBase() { return *this; }
  const BaseTy &getBase() const { return *this; }

  llvm::SmallVector<isl::union_set, 8> ASTBuildOptions;

  void visitBand(isl::schedule_node_band Band) {
    ASTBuildOptions.push_back(
        isl::manage(isl_schedule_node_band_get_ast_build_options(Band.get())));
    return getBase().visitBand(Band);
  }
};

/// Apply AST build options to the bands in a schedule tree.
///
/// This rewrites a schedule tree with the AST build options applied. We assume
/// that the band nodes are visited in the same order as they were when the
/// build options were collected, typically by CollectASTBuildOptions.
struct ApplyASTBuildOptions final : ScheduleNodeRewriter<ApplyASTBuildOptions> {
  using BaseTy = ScheduleNodeRewriter<ApplyASTBuildOptions>;
  BaseTy &getBase() { return *this; }
  const BaseTy &getBase() const { return *this; }

  size_t Pos;
  llvm::ArrayRef<isl::union_set> ASTBuildOptions;

  ApplyASTBuildOptions(llvm::ArrayRef<isl::union_set> ASTBuildOptions)
      : ASTBuildOptions(ASTBuildOptions) {}

  isl::schedule visitSchedule(isl::schedule Schedule) {
    Pos = 0;
    isl::schedule Result = visit(Schedule).get_schedule();
    assert(Pos == ASTBuildOptions.size() &&
           "AST build options must match to band nodes");
    return Result;
  }

  isl::schedule_node visitBand(isl::schedule_node_band Band) {
    isl::schedule_node_band Result =
        Band.set_ast_build_options(ASTBuildOptions[Pos]);
    Pos += 1;
    return getBase().visitBand(Result);
  }
};

/// Return whether the schedule contains an extension node.
static bool containsExtensionNode(isl::schedule Schedule) {
  assert(!Schedule.is_null());

  auto Callback = [](__isl_keep isl_schedule_node *Node,
                     void *User) -> isl_bool {
    if (isl_schedule_node_get_type(Node) == isl_schedule_node_extension) {
      // Stop walking the schedule tree.
      return isl_bool_error;
    }

    // Continue searching the subtree.
    return isl_bool_true;
  };
  isl_stat RetVal = isl_schedule_foreach_schedule_node_top_down(
      Schedule.get(), Callback, nullptr);

  // We assume that the traversal itself does not fail, i.e. the only reason to
  // return isl_stat_error is that an extension node was found.
  return RetVal == isl_stat_error;
}

/// Find a named MDNode property in a LoopID.
static MDNode *findOptionalNodeOperand(MDNode *LoopMD, StringRef Name) {
  return dyn_cast_or_null<MDNode>(
      findMetadataOperand(LoopMD, Name).value_or(nullptr));
}

/// Is this node of type mark?
static bool isMark(const isl::schedule_node &Node) {
  return isl_schedule_node_get_type(Node.get()) == isl_schedule_node_mark;
}

/// Is this node of type band?
static bool isBand(const isl::schedule_node &Node) {
  return isl_schedule_node_get_type(Node.get()) == isl_schedule_node_band;
}

#ifndef NDEBUG
/// Is this node a band of a single dimension (i.e. could represent a loop)?
static bool isBandWithSingleLoop(const isl::schedule_node &Node) {
  return isBand(Node) && isl_schedule_node_band_n_member(Node.get()) == 1;
}
#endif

static bool isLeaf(const isl::schedule_node &Node) {
  return isl_schedule_node_get_type(Node.get()) == isl_schedule_node_leaf;
}

/// Create an isl::id representing the output loop after a transformation.
static isl::id createGeneratedLoopAttr(isl::ctx Ctx, MDNode *FollowupLoopMD) {
  // Don't need to id the followup.
  // TODO: Append llvm.loop.disable_heustistics metadata unless overridden by
  //       user followup-MD
  if (!FollowupLoopMD)
    return {};

  BandAttr *Attr = new BandAttr();
  Attr->Metadata = FollowupLoopMD;
  return getIslLoopAttr(Ctx, Attr);
}

/// A loop consists of a band and an optional marker that wraps it. Return the
/// outermost of the two.

/// That is, either the mark or, if there is not mark, the loop itself. Can
/// start with either the mark or the band.
static isl::schedule_node moveToBandMark(isl::schedule_node BandOrMark) {
  if (isBandMark(BandOrMark)) {
    assert(isBandWithSingleLoop(BandOrMark.child(0)));
    return BandOrMark;
  }
  assert(isBandWithSingleLoop(BandOrMark));

  isl::schedule_node Mark = BandOrMark.parent();
  if (isBandMark(Mark))
    return Mark;

  // Band has no loop marker.
  return BandOrMark;
}

static isl::schedule_node removeMark(isl::schedule_node MarkOrBand,
                                     BandAttr *&Attr) {
  MarkOrBand = moveToBandMark(MarkOrBand);

  isl::schedule_node Band;
  if (isMark(MarkOrBand)) {
    Attr = getLoopAttr(MarkOrBand.as<isl::schedule_node_mark>().get_id());
    Band = isl::manage(isl_schedule_node_delete(MarkOrBand.release()));
  } else {
    Attr = nullptr;
    Band = MarkOrBand;
  }

  assert(isBandWithSingleLoop(Band));
  return Band;
}

/// Remove the mark that wraps a loop. Return the band representing the loop.
static isl::schedule_node removeMark(isl::schedule_node MarkOrBand) {
  BandAttr *Attr;
  return removeMark(MarkOrBand, Attr);
}

static isl::schedule_node insertMark(isl::schedule_node Band, isl::id Mark) {
  assert(isBand(Band));
  assert(moveToBandMark(Band).is_equal(Band) &&
         "Don't add a two marks for a band");

  return Band.insert_mark(Mark).child(0);
}

/// Return the (one-dimensional) set of numbers that are divisible by @p Factor
/// with remainder @p Offset.
///
///  isDivisibleBySet(Ctx, 4, 0) = { [i] : floord(i,4) = 0 }
///  isDivisibleBySet(Ctx, 4, 1) = { [i] : floord(i,4) = 1 }
///
static isl::basic_set isDivisibleBySet(isl::ctx &Ctx, long Factor,
                                       long Offset) {
  isl::val ValFactor{Ctx, Factor};
  isl::val ValOffset{Ctx, Offset};

  isl::space Unispace{Ctx, 0, 1};
  isl::local_space LUnispace{Unispace};
  isl::aff AffFactor{LUnispace, ValFactor};
  isl::aff AffOffset{LUnispace, ValOffset};

  isl::aff Id = isl::aff::var_on_domain(LUnispace, isl::dim::out, 0);
  isl::aff DivMul = Id.mod(ValFactor);
  isl::basic_map Divisible = isl::basic_map::from_aff(DivMul);
  isl::basic_map Modulo = Divisible.fix_val(isl::dim::out, 0, ValOffset);
  return Modulo.domain();
}

/// Make the last dimension of Set to take values from 0 to VectorWidth - 1.
///
/// @param Set         A set, which should be modified.
/// @param VectorWidth A parameter, which determines the constraint.
static isl::set addExtentConstraints(isl::set Set, int VectorWidth) {
  unsigned Dims = unsignedFromIslSize(Set.tuple_dim());
  assert(Dims >= 1);
  isl::space Space = Set.get_space();
  isl::local_space LocalSpace = isl::local_space(Space);
  isl::constraint ExtConstr = isl::constraint::alloc_inequality(LocalSpace);
  ExtConstr = ExtConstr.set_constant_si(0);
  ExtConstr = ExtConstr.set_coefficient_si(isl::dim::set, Dims - 1, 1);
  Set = Set.add_constraint(ExtConstr);
  ExtConstr = isl::constraint::alloc_inequality(LocalSpace);
  ExtConstr = ExtConstr.set_constant_si(VectorWidth - 1);
  ExtConstr = ExtConstr.set_coefficient_si(isl::dim::set, Dims - 1, -1);
  return Set.add_constraint(ExtConstr);
}

/// Collapse perfectly nested bands into a single band.
class BandCollapseRewriter final
    : public ScheduleTreeRewriter<BandCollapseRewriter> {
private:
  using BaseTy = ScheduleTreeRewriter<BandCollapseRewriter>;
  BaseTy &getBase() { return *this; }
  const BaseTy &getBase() const { return *this; }

public:
  isl::schedule visitBand(isl::schedule_node_band RootBand) {
    isl::schedule_node_band Band = RootBand;
    isl::ctx Ctx = Band.ctx();

    // Do not merge permutable band to avoid loosing the permutability property.
    // Cannot collapse even two permutable loops, they might be permutable
    // individually, but not necassarily across.
    if (unsignedFromIslSize(Band.n_member()) > 1u && Band.permutable())
      return getBase().visitBand(Band);

    // Find collapsable bands.
    SmallVector<isl::schedule_node_band> Nest;
    int NumTotalLoops = 0;
    isl::schedule_node Body;
    while (true) {
      Nest.push_back(Band);
      NumTotalLoops += unsignedFromIslSize(Band.n_member());
      Body = Band.first_child();
      if (!Body.isa<isl::schedule_node_band>())
        break;
      Band = Body.as<isl::schedule_node_band>();

      // Do not include next band if it is permutable to not lose its
      // permutability property.
      if (unsignedFromIslSize(Band.n_member()) > 1u && Band.permutable())
        break;
    }

    // Nothing to collapse, preserve permutability.
    if (Nest.size() <= 1)
      return getBase().visitBand(Band);

    LLVM_DEBUG({
      dbgs() << "Found loops to collapse between\n";
      dumpIslObj(RootBand, dbgs());
      dbgs() << "and\n";
      dumpIslObj(Body, dbgs());
      dbgs() << "\n";
    });

    isl::schedule NewBody = visit(Body);

    // Collect partial schedules from all members.
    isl::union_pw_aff_list PartScheds{Ctx, NumTotalLoops};
    for (isl::schedule_node_band Band : Nest) {
      int NumLoops = unsignedFromIslSize(Band.n_member());
      isl::multi_union_pw_aff BandScheds = Band.get_partial_schedule();
      for (auto j : seq<int>(0, NumLoops))
        PartScheds = PartScheds.add(BandScheds.at(j));
    }
    isl::space ScatterSpace = isl::space(Ctx, 0, NumTotalLoops);
    isl::multi_union_pw_aff PartSchedsMulti{ScatterSpace, PartScheds};

    isl::schedule_node_band CollapsedBand =
        NewBody.insert_partial_schedule(PartSchedsMulti)
            .get_root()
            .first_child()
            .as<isl::schedule_node_band>();

    // Copy over loop attributes form original bands.
    int LoopIdx = 0;
    for (isl::schedule_node_band Band : Nest) {
      int NumLoops = unsignedFromIslSize(Band.n_member());
      for (int i : seq<int>(0, NumLoops)) {
        CollapsedBand = applyBandMemberAttributes(std::move(CollapsedBand),
                                                  LoopIdx, Band, i);
        LoopIdx += 1;
      }
    }
    assert(LoopIdx == NumTotalLoops &&
           "Expect the same number of loops to add up again");

    return CollapsedBand.get_schedule();
  }
};

static isl::schedule collapseBands(isl::schedule Sched) {
  LLVM_DEBUG(dbgs() << "Collapse bands in schedule\n");
  BandCollapseRewriter Rewriter;
  return Rewriter.visit(Sched);
}

/// Collect sequentially executed bands (or anything else), even if nested in a
/// mark or other nodes whose child is executed just once. If we can
/// successfully fuse the bands, we allow them to be removed.
static void collectPotentiallyFusableBands(
    isl::schedule_node Node,
    SmallVectorImpl<std::pair<isl::schedule_node, isl::schedule_node>>
        &ScheduleBands,
    const isl::schedule_node &DirectChild) {
  switch (isl_schedule_node_get_type(Node.get())) {
  case isl_schedule_node_sequence:
  case isl_schedule_node_set:
  case isl_schedule_node_mark:
  case isl_schedule_node_domain:
  case isl_schedule_node_filter:
    if (Node.has_children()) {
      isl::schedule_node C = Node.first_child();
      while (true) {
        collectPotentiallyFusableBands(C, ScheduleBands, DirectChild);
        if (!C.has_next_sibling())
          break;
        C = C.next_sibling();
      }
    }
    break;

  default:
    // Something that does not execute suquentially (e.g. a band)
    ScheduleBands.push_back({Node, DirectChild});
    break;
  }
}

/// Remove dependencies that are resolved by @p PartSched. That is, remove
/// everything that we already know is executed in-order.
static isl::union_map remainingDepsFromPartialSchedule(isl::union_map PartSched,
                                                       isl::union_map Deps) {
  unsigned NumDims = getNumScatterDims(PartSched);
  auto ParamSpace = PartSched.get_space().params();

  // { Scatter[] }
  isl::space ScatterSpace =
      ParamSpace.set_from_params().add_dims(isl::dim::set, NumDims);

  // { Scatter[] -> Domain[] }
  isl::union_map PartSchedRev = PartSched.reverse();

  // { Scatter[] -> Scatter[] }
  isl::map MaybeBefore = isl::map::lex_le(ScatterSpace);

  // { Domain[] -> Domain[] }
  isl::union_map DomMaybeBefore =
      MaybeBefore.apply_domain(PartSchedRev).apply_range(PartSchedRev);

  // { Domain[] -> Domain[] }
  isl::union_map ChildRemainingDeps = Deps.intersect(DomMaybeBefore);

  return ChildRemainingDeps;
}

/// Remove dependencies that are resolved by executing them in the order
/// specified by @p Domains;
static isl::union_map remainigDepsFromSequence(ArrayRef<isl::union_set> Domains,
                                               isl::union_map Deps) {
  isl::ctx Ctx = Deps.ctx();
  isl::space ParamSpace = Deps.get_space().params();

  // Create a partial schedule mapping to constants that reflect the execution
  // order.
  isl::union_map PartialSchedules = isl::union_map::empty(Ctx);
  for (auto P : enumerate(Domains)) {
    isl::val ExecTime = isl::val(Ctx, P.index());
    isl::union_pw_aff DomSched{P.value(), ExecTime};
    PartialSchedules = PartialSchedules.unite(DomSched.as_union_map());
  }

  return remainingDepsFromPartialSchedule(PartialSchedules, Deps);
}

/// Determine whether the outermost loop of to bands can be fused while
/// respecting validity dependencies.
static bool canFuseOutermost(const isl::schedule_node_band &LHS,
                             const isl::schedule_node_band &RHS,
                             const isl::union_map &Deps) {
  // { LHSDomain[] -> Scatter[] }
  isl::union_map LHSPartSched =
      LHS.get_partial_schedule().get_at(0).as_union_map();

  // { Domain[] -> Scatter[] }
  isl::union_map RHSPartSched =
      RHS.get_partial_schedule().get_at(0).as_union_map();

  // Dependencies that are already resolved because LHS executes before RHS, but
  // will not be anymore after fusion. { DefDomain[] -> UseDomain[] }
  isl::union_map OrderedBySequence =
      Deps.intersect_domain(LHSPartSched.domain())
          .intersect_range(RHSPartSched.domain());

  isl::space ParamSpace = OrderedBySequence.get_space().params();
  isl::space NewScatterSpace = ParamSpace.add_unnamed_tuple(1);

  // { Scatter[] -> Scatter[] }
  isl::map After = isl::map::lex_gt(NewScatterSpace);

  // After fusion, instances with smaller (or equal, which means they will be
  // executed in the same iteration, but the LHS instance is still sequenced
  // before RHS) scatter value will still be executed before. This are the
  // orderings where this is not necessarily the case.
  // { LHSDomain[] -> RHSDomain[] }
  isl::union_map MightBeAfterDoms = After.apply_domain(LHSPartSched.reverse())
                                        .apply_range(RHSPartSched.reverse());

  // Dependencies that are not resolved by the new execution order.
  isl::union_map WithBefore = OrderedBySequence.intersect(MightBeAfterDoms);

  return WithBefore.is_empty();
}

/// Fuse @p LHS and @p RHS if possible while preserving validity dependenvies.
static isl::schedule tryGreedyFuse(isl::schedule_node_band LHS,
                                   isl::schedule_node_band RHS,
                                   const isl::union_map &Deps) {
  if (!canFuseOutermost(LHS, RHS, Deps))
    return {};

  LLVM_DEBUG({
    dbgs() << "Found loops for greedy fusion:\n";
    dumpIslObj(LHS, dbgs());
    dbgs() << "and\n";
    dumpIslObj(RHS, dbgs());
    dbgs() << "\n";
  });

  // The partial schedule of the bands outermost loop that we need to combine
  // for the fusion.
  isl::union_pw_aff LHSPartOuterSched = LHS.get_partial_schedule().get_at(0);
  isl::union_pw_aff RHSPartOuterSched = RHS.get_partial_schedule().get_at(0);

  // Isolate band bodies as roots of their own schedule trees.
  IdentityRewriter Rewriter;
  isl::schedule LHSBody = Rewriter.visit(LHS.first_child());
  isl::schedule RHSBody = Rewriter.visit(RHS.first_child());

  // Reconstruct the non-outermost (not going to be fused) loops from both
  // bands.
  // TODO: Maybe it is possibly to transfer the 'permutability' property from
  // LHS+RHS. At minimum we need merge multiple band members at once, otherwise
  // permutability has no meaning.
  isl::schedule LHSNewBody =
      rebuildBand(LHS, LHSBody, [](int i) { return i > 0; });
  isl::schedule RHSNewBody =
      rebuildBand(RHS, RHSBody, [](int i) { return i > 0; });

  // The loop body of the fused loop.
  isl::schedule NewCommonBody = LHSNewBody.sequence(RHSNewBody);

  // Combine the partial schedules of both loops to a new one. Instances with
  // the same scatter value are put together.
  isl::union_map NewCommonPartialSched =
      LHSPartOuterSched.as_union_map().unite(RHSPartOuterSched.as_union_map());
  isl::schedule NewCommonSchedule = NewCommonBody.insert_partial_schedule(
      NewCommonPartialSched.as_multi_union_pw_aff());

  return NewCommonSchedule;
}

static isl::schedule tryGreedyFuse(isl::schedule_node LHS,
                                   isl::schedule_node RHS,
                                   const isl::union_map &Deps) {
  // TODO: Non-bands could be interpreted as a band with just as single
  // iteration. However, this is only useful if both ends of a fused loop were
  // originally loops themselves.
  if (!LHS.isa<isl::schedule_node_band>())
    return {};
  if (!RHS.isa<isl::schedule_node_band>())
    return {};

  return tryGreedyFuse(LHS.as<isl::schedule_node_band>(),
                       RHS.as<isl::schedule_node_band>(), Deps);
}

/// Fuse all fusable loop top-down in a schedule tree.
///
/// The isl::union_map parameters is the set of validity dependencies that have
/// not been resolved/carried by a parent schedule node.
class GreedyFusionRewriter final
    : public ScheduleTreeRewriter<GreedyFusionRewriter, isl::union_map> {
private:
  using BaseTy = ScheduleTreeRewriter<GreedyFusionRewriter, isl::union_map>;
  BaseTy &getBase() { return *this; }
  const BaseTy &getBase() const { return *this; }

public:
  /// Is set to true if anything has been fused.
  bool AnyChange = false;

  isl::schedule visitBand(isl::schedule_node_band Band, isl::union_map Deps) {
    // { Domain[] -> Scatter[] }
    isl::union_map PartSched =
        isl::union_map::from(Band.get_partial_schedule());
    assert(getNumScatterDims(PartSched) ==
           unsignedFromIslSize(Band.n_member()));
    isl::space ParamSpace = PartSched.get_space().params();

    // { Scatter[] -> Domain[] }
    isl::union_map PartSchedRev = PartSched.reverse();

    // Possible within the same iteration. Dependencies with smaller scatter
    // value are carried by this loop and therefore have been resolved by the
    // in-order execution if the loop iteration. A dependency with small scatter
    // value would be a dependency violation that we assume did not happen. {
    // Domain[] -> Domain[] }
    isl::union_map Unsequenced = PartSchedRev.apply_domain(PartSchedRev);

    // Actual dependencies within the same iteration.
    // { DefDomain[] -> UseDomain[] }
    isl::union_map RemDeps = Deps.intersect(Unsequenced);

    return getBase().visitBand(Band, RemDeps);
  }

  isl::schedule visitSequence(isl::schedule_node_sequence Sequence,
                              isl::union_map Deps) {
    int NumChildren = isl_schedule_node_n_children(Sequence.get());

    // List of fusion candidates. The first element is the fusion candidate, the
    // second is candidate's ancestor that is the sequence's direct child. It is
    // preferable to use the direct child if not if its non-direct children is
    // fused to preserve its structure such as mark nodes.
    SmallVector<std::pair<isl::schedule_node, isl::schedule_node>> Bands;
    for (auto i : seq<int>(0, NumChildren)) {
      isl::schedule_node Child = Sequence.child(i);
      collectPotentiallyFusableBands(Child, Bands, Child);
    }

    // Direct children that had at least one of its decendants fused.
    SmallDenseSet<isl_schedule_node *, 4> ChangedDirectChildren;

    // Fuse neigboring bands until reaching the end of candidates.
    int i = 0;
    while (i + 1 < (int)Bands.size()) {
      isl::schedule Fused =
          tryGreedyFuse(Bands[i].first, Bands[i + 1].first, Deps);
      if (Fused.is_null()) {
        // Cannot merge this node with the next; look at next pair.
        i += 1;
        continue;
      }

      // Mark the direct children as (partially) fused.
      if (!Bands[i].second.is_null())
        ChangedDirectChildren.insert(Bands[i].second.get());
      if (!Bands[i + 1].second.is_null())
        ChangedDirectChildren.insert(Bands[i + 1].second.get());

      // Collapse the neigbros to a single new candidate that could be fused
      // with the next candidate.
      Bands[i] = {Fused.get_root(), {}};
      Bands.erase(Bands.begin() + i + 1);

      AnyChange = true;
    }

    // By construction equal if done with collectPotentiallyFusableBands's
    // output.
    SmallVector<isl::union_set> SubDomains;
    SubDomains.reserve(NumChildren);
    for (int i = 0; i < NumChildren; i += 1)
      SubDomains.push_back(Sequence.child(i).domain());
    auto SubRemainingDeps = remainigDepsFromSequence(SubDomains, Deps);

    // We may iterate over direct children multiple times, be sure to add each
    // at most once.
    SmallDenseSet<isl_schedule_node *, 4> AlreadyAdded;

    isl::schedule Result;
    for (auto &P : Bands) {
      isl::schedule_node MaybeFused = P.first;
      isl::schedule_node DirectChild = P.second;

      // If not modified, use the direct child.
      if (!DirectChild.is_null() &&
          !ChangedDirectChildren.count(DirectChild.get())) {
        if (AlreadyAdded.count(DirectChild.get()))
          continue;
        AlreadyAdded.insert(DirectChild.get());
        MaybeFused = DirectChild;
      } else {
        assert(AnyChange &&
               "Need changed flag for be consistent with actual change");
      }

      // Top-down recursion: If the outermost loop has been fused, their nested
      // bands might be fusable now as well.
      isl::schedule InnerFused = visit(MaybeFused, SubRemainingDeps);

      // Reconstruct the sequence, with some of the children fused.
      if (Result.is_null())
        Result = InnerFused;
      else
        Result = Result.sequence(InnerFused);
    }

    return Result;
  }
};

} // namespace

bool polly::isBandMark(const isl::schedule_node &Node) {
  return isMark(Node) &&
         isLoopAttr(Node.as<isl::schedule_node_mark>().get_id());
}

BandAttr *polly::getBandAttr(isl::schedule_node MarkOrBand) {
  MarkOrBand = moveToBandMark(MarkOrBand);
  if (!isMark(MarkOrBand))
    return nullptr;

  return getLoopAttr(MarkOrBand.as<isl::schedule_node_mark>().get_id());
}

isl::schedule polly::hoistExtensionNodes(isl::schedule Sched) {
  // If there is no extension node in the first place, return the original
  // schedule tree.
  if (!containsExtensionNode(Sched))
    return Sched;

  // Build options can anchor schedule nodes, such that the schedule tree cannot
  // be modified anymore. Therefore, apply build options after the tree has been
  // created.
  CollectASTBuildOptions Collector;
  Collector.visit(Sched);

  // Rewrite the schedule tree without extension nodes.
  ExtensionNodeRewriter Rewriter;
  isl::schedule NewSched = Rewriter.visitSchedule(Sched);

  // Reapply the AST build options. The rewriter must not change the iteration
  // order of bands. Any other node type is ignored.
  ApplyASTBuildOptions Applicator(Collector.ASTBuildOptions);
  NewSched = Applicator.visitSchedule(NewSched);

  return NewSched;
}

isl::schedule polly::applyFullUnroll(isl::schedule_node BandToUnroll) {
  isl::ctx Ctx = BandToUnroll.ctx();

  // Remove the loop's mark, the loop will disappear anyway.
  BandToUnroll = removeMark(BandToUnroll);
  assert(isBandWithSingleLoop(BandToUnroll));

  isl::multi_union_pw_aff PartialSched = isl::manage(
      isl_schedule_node_band_get_partial_schedule(BandToUnroll.get()));
  assert(unsignedFromIslSize(PartialSched.dim(isl::dim::out)) == 1u &&
         "Can only unroll a single dimension");
  isl::union_pw_aff PartialSchedUAff = PartialSched.at(0);

  isl::union_set Domain = BandToUnroll.get_domain();
  PartialSchedUAff = PartialSchedUAff.intersect_domain(Domain);
  isl::union_map PartialSchedUMap =
      isl::union_map::from(isl::union_pw_multi_aff(PartialSchedUAff));

  // Enumerator only the scatter elements.
  isl::union_set ScatterList = PartialSchedUMap.range();

  // Enumerate all loop iterations.
  // TODO: Diagnose if not enumerable or depends on a parameter.
  SmallVector<isl::point, 16> Elts;
  ScatterList.foreach_point([&Elts](isl::point P) -> isl::stat {
    Elts.push_back(P);
    return isl::stat::ok();
  });

  // Don't assume that foreach_point returns in execution order.
  llvm::sort(Elts, [](isl::point P1, isl::point P2) -> bool {
    isl::val C1 = P1.get_coordinate_val(isl::dim::set, 0);
    isl::val C2 = P2.get_coordinate_val(isl::dim::set, 0);
    return C1.lt(C2);
  });

  // Convert the points to a sequence of filters.
  isl::union_set_list List = isl::union_set_list(Ctx, Elts.size());
  for (isl::point P : Elts) {
    // Determine the domains that map this scatter element.
    isl::union_set DomainFilter = PartialSchedUMap.intersect_range(P).domain();

    List = List.add(DomainFilter);
  }

  // Replace original band with unrolled sequence.
  isl::schedule_node Body =
      isl::manage(isl_schedule_node_delete(BandToUnroll.release()));
  Body = Body.insert_sequence(List);
  return Body.get_schedule();
}

isl::schedule polly::applyPartialUnroll(isl::schedule_node BandToUnroll,
                                        int Factor) {
  assert(Factor > 0 && "Positive unroll factor required");
  isl::ctx Ctx = BandToUnroll.ctx();

  // Remove the mark, save the attribute for later use.
  BandAttr *Attr;
  BandToUnroll = removeMark(BandToUnroll, Attr);
  assert(isBandWithSingleLoop(BandToUnroll));

  isl::multi_union_pw_aff PartialSched = isl::manage(
      isl_schedule_node_band_get_partial_schedule(BandToUnroll.get()));

  // { Stmt[] -> [x] }
  isl::union_pw_aff PartialSchedUAff = PartialSched.at(0);

  // Here we assume the schedule stride is one and starts with 0, which is not
  // necessarily the case.
  isl::union_pw_aff StridedPartialSchedUAff =
      isl::union_pw_aff::empty(PartialSchedUAff.get_space());
  isl::val ValFactor{Ctx, Factor};
  PartialSchedUAff.foreach_pw_aff([&StridedPartialSchedUAff,
                                   &ValFactor](isl::pw_aff PwAff) -> isl::stat {
    isl::space Space = PwAff.get_space();
    isl::set Universe = isl::set::universe(Space.domain());
    isl::pw_aff AffFactor{Universe, ValFactor};
    isl::pw_aff DivSchedAff = PwAff.div(AffFactor).floor().mul(AffFactor);
    StridedPartialSchedUAff = StridedPartialSchedUAff.union_add(DivSchedAff);
    return isl::stat::ok();
  });

  isl::union_set_list List = isl::union_set_list(Ctx, Factor);
  for (auto i : seq<int>(0, Factor)) {
    // { Stmt[] -> [x] }
    isl::union_map UMap =
        isl::union_map::from(isl::union_pw_multi_aff(PartialSchedUAff));

    // { [x] }
    isl::basic_set Divisible = isDivisibleBySet(Ctx, Factor, i);

    // { Stmt[] }
    isl::union_set UnrolledDomain = UMap.intersect_range(Divisible).domain();

    List = List.add(UnrolledDomain);
  }

  isl::schedule_node Body =
      isl::manage(isl_schedule_node_delete(BandToUnroll.copy()));
  Body = Body.insert_sequence(List);
  isl::schedule_node NewLoop =
      Body.insert_partial_schedule(StridedPartialSchedUAff);

  MDNode *FollowupMD = nullptr;
  if (Attr && Attr->Metadata)
    FollowupMD =
        findOptionalNodeOperand(Attr->Metadata, LLVMLoopUnrollFollowupUnrolled);

  isl::id NewBandId = createGeneratedLoopAttr(Ctx, FollowupMD);
  if (!NewBandId.is_null())
    NewLoop = insertMark(NewLoop, NewBandId);

  return NewLoop.get_schedule();
}

isl::set polly::getPartialTilePrefixes(isl::set ScheduleRange,
                                       int VectorWidth) {
  unsigned Dims = unsignedFromIslSize(ScheduleRange.tuple_dim());
  assert(Dims >= 1);
  isl::set LoopPrefixes =
      ScheduleRange.drop_constraints_involving_dims(isl::dim::set, Dims - 1, 1);
  auto ExtentPrefixes = addExtentConstraints(LoopPrefixes, VectorWidth);
  isl::set BadPrefixes = ExtentPrefixes.subtract(ScheduleRange);
  BadPrefixes = BadPrefixes.project_out(isl::dim::set, Dims - 1, 1);
  LoopPrefixes = LoopPrefixes.project_out(isl::dim::set, Dims - 1, 1);
  return LoopPrefixes.subtract(BadPrefixes);
}

isl::union_set polly::getIsolateOptions(isl::set IsolateDomain,
                                        unsigned OutDimsNum) {
  unsigned Dims = unsignedFromIslSize(IsolateDomain.tuple_dim());
  assert(OutDimsNum <= Dims &&
         "The isl::set IsolateDomain is used to describe the range of schedule "
         "dimensions values, which should be isolated. Consequently, the "
         "number of its dimensions should be greater than or equal to the "
         "number of the schedule dimensions.");
  isl::map IsolateRelation = isl::map::from_domain(IsolateDomain);
  IsolateRelation = IsolateRelation.move_dims(isl::dim::out, 0, isl::dim::in,
                                              Dims - OutDimsNum, OutDimsNum);
  isl::set IsolateOption = IsolateRelation.wrap();
  isl::id Id = isl::id::alloc(IsolateOption.ctx(), "isolate", nullptr);
  IsolateOption = IsolateOption.set_tuple_id(Id);
  return isl::union_set(IsolateOption);
}

isl::union_set polly::getDimOptions(isl::ctx Ctx, const char *Option) {
  isl::space Space(Ctx, 0, 1);
  auto DimOption = isl::set::universe(Space);
  auto Id = isl::id::alloc(Ctx, Option, nullptr);
  DimOption = DimOption.set_tuple_id(Id);
  return isl::union_set(DimOption);
}

isl::schedule_node polly::tileNode(isl::schedule_node Node,
                                   const char *Identifier,
                                   ArrayRef<int> TileSizes,
                                   int DefaultTileSize) {
  auto Space = isl::manage(isl_schedule_node_band_get_space(Node.get()));
  auto Dims = Space.dim(isl::dim::set);
  auto Sizes = isl::multi_val::zero(Space);
  std::string IdentifierString(Identifier);
  for (unsigned i : rangeIslSize(0, Dims)) {
    unsigned tileSize = i < TileSizes.size() ? TileSizes[i] : DefaultTileSize;
    Sizes = Sizes.set_val(i, isl::val(Node.ctx(), tileSize));
  }
  auto TileLoopMarkerStr = IdentifierString + " - Tiles";
  auto TileLoopMarker = isl::id::alloc(Node.ctx(), TileLoopMarkerStr, nullptr);
  Node = Node.insert_mark(TileLoopMarker);
  Node = Node.child(0);
  Node =
      isl::manage(isl_schedule_node_band_tile(Node.release(), Sizes.release()));
  Node = Node.child(0);
  auto PointLoopMarkerStr = IdentifierString + " - Points";
  auto PointLoopMarker =
      isl::id::alloc(Node.ctx(), PointLoopMarkerStr, nullptr);
  Node = Node.insert_mark(PointLoopMarker);
  return Node.child(0);
}

isl::schedule_node polly::applyRegisterTiling(isl::schedule_node Node,
                                              ArrayRef<int> TileSizes,
                                              int DefaultTileSize) {
  Node = tileNode(Node, "Register tiling", TileSizes, DefaultTileSize);
  auto Ctx = Node.ctx();
  return Node.as<isl::schedule_node_band>().set_ast_build_options(
      isl::union_set(Ctx, "{unroll[x]}"));
}

/// Find statements and sub-loops in (possibly nested) sequences.
static void
collectFissionableStmts(isl::schedule_node Node,
                        SmallVectorImpl<isl::schedule_node> &ScheduleStmts) {
  if (isBand(Node) || isLeaf(Node)) {
    ScheduleStmts.push_back(Node);
    return;
  }

  if (Node.has_children()) {
    isl::schedule_node C = Node.first_child();
    while (true) {
      collectFissionableStmts(C, ScheduleStmts);
      if (!C.has_next_sibling())
        break;
      C = C.next_sibling();
    }
  }
}

isl::schedule polly::applyMaxFission(isl::schedule_node BandToFission) {
  isl::ctx Ctx = BandToFission.ctx();
  BandToFission = removeMark(BandToFission);
  isl::schedule_node BandBody = BandToFission.child(0);

  SmallVector<isl::schedule_node> FissionableStmts;
  collectFissionableStmts(BandBody, FissionableStmts);
  size_t N = FissionableStmts.size();

  // Collect the domain for each of the statements that will get their own loop.
  isl::union_set_list DomList = isl::union_set_list(Ctx, N);
  for (size_t i = 0; i < N; ++i) {
    isl::schedule_node BodyPart = FissionableStmts[i];
    DomList = DomList.add(BodyPart.get_domain());
  }

  // Apply the fission by copying the entire loop, but inserting a filter for
  // the statement domains for each fissioned loop.
  isl::schedule_node Fissioned = BandToFission.insert_sequence(DomList);

  return Fissioned.get_schedule();
}

isl::schedule polly::applyGreedyFusion(isl::schedule Sched,
                                       const isl::union_map &Deps) {
  LLVM_DEBUG(dbgs() << "Greedy loop fusion\n");

  GreedyFusionRewriter Rewriter;
  isl::schedule Result = Rewriter.visit(Sched, Deps);
  if (!Rewriter.AnyChange) {
    LLVM_DEBUG(dbgs() << "Found nothing to fuse\n");
    return Sched;
  }

  // GreedyFusionRewriter due to working loop-by-loop, bands with multiple loops
  // may have been split into multiple bands.
  return collapseBands(Result);
}
