/*
 * Copyright 2024 WebAssembly Community Group participants
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Split types into minimal recursion groups by computing the strongly connected
// components of the type graph, which are the minimal sets of
// mutually-recursive types that must be in the same recursion group with each
// other for the type section to validate.
//
// We must additionally ensure that distinct types remain distinct, which would
// not be the case if we allowed two separate strongly connected components with
// the same shape to be rewritten to two copies of the same recursion group.
// Accidentally merging types would be incorrect because it would change the
// behavior of casts that would have previously been able to differentiate the
// types.
//
// When multiple strongly connected components have the same shape, first try to
// differentiate them by permuting their types, which keeps the sizes of the rec
// groups as small as possible. When there are more strongly connected
// components that are permutations of one another than there are distinct
// permutations, fall back to keeping the types distinct by adding distinct
// brand types to the recursion groups to ensure they have different shapes.
//
// There are several possible algorithmic design for detecting when to generate
// permutations and determining what permutations to generate. They trade off
// the amount of "wasted" work spent generating shapes that have already been
// used with the amount of work spent trying to avoid the wasted work and with
// the quality of the output.
//
// The simplest possible design that avoids all "wasted" work would be to
// canonicalize the shape of each group to eagerly detect isomorphisms and
// eagerly organize the groups into equivalence classes. This would allow us to
// avoid ever generating permutations of a group with shapes that have already
// been used. See `getCanonicalPermutation` below for details on how this works.
// However, this is not an ideal solution because canonicalization is an
// expensive operation and in the common case a group's shape will not conflict
// with any other group's shape, so canonicalization itself would be wasted
// work.
//
// The simplest possible design in the other direction is to never canonicalize
// and instead to lazily build up equivalences classes of isomorphic groups when
// shape conflicts are detected in practice. Conflicts are resolved by choosing
// the next permutation generated by the representative element of the
// equivalence class. This solution is not ideal because groups with high
// symmetry can have very many permutations that have the same shape, so many
// permutations might need to be generated before finding one that is distinct
// from previous permutations with respect to its shape. This work can be
// sidestepped by instead eagerly advancing the brand type, but that increases
// the code size of the output.
//
// The design chosen here is a hybrid of those two designs that is slightly more
// complicated, but gets the best of both worlds. We lazily detect equivalence
// classes of groups without eager shape canonicalization like in the second
// design, but we canonicalize the shape for any equivalence class that grows
// larger than one group like in the first design. This ensures that we don't
// spend time canonicalizing in the common case where there are no shape
// conflicts, but it also ensures that we don't waste time generating
// permutations with shapes that conflict with the shapes of previous groups. It
// also allows us to avoid compromising on the quality of the output.

#include "ir/module-utils.h"
#include "ir/type-updating.h"
#include "pass.h"
#include "support/disjoint_sets.h"
#include "support/strongly_connected_components.h"
#include "support/topological_sort.h"
#include "wasm-type-shape.h"
#include "wasm.h"

namespace wasm {

namespace {

// Compute the strongly connected components of the private types, being sure to
// only consider edges to other private types.
struct TypeSCCs
  : SCCs<typename std::vector<HeapType>::const_iterator, TypeSCCs> {
  std::unordered_set<HeapType> includedTypes;
  TypeSCCs(const std::vector<HeapType>& types)
    : SCCs(types.begin(), types.end()),
      includedTypes(types.cbegin(), types.cend()) {}
  void pushChildren(HeapType parent) {
    for (auto child : parent.getReferencedHeapTypes()) {
      if (includedTypes.count(child)) {
        push(child);
      }
    }
  }
};

// After all their permutations with distinct shapes have been used, different
// groups with the same shapes must be differentiated by adding in a "brand"
// type. Even with a brand mixed in, we might run out of permutations with
// distinct shapes, in which case we need a new brand type. This iterator
// provides an infinite sequence of possible brand types, prioritizing those
// with the most compact encoding.
struct BrandTypeIterator {
  // See `initFieldOptions` for the 18 options.
  static constexpr Index optionCount = 18;
  static std::array<Field, optionCount> fieldOptions;
  static void initFieldOptions();

  struct FieldInfo {
    uint8_t index = 0;
    bool immutable = false;

    operator Field() const {
      auto field = fieldOptions[index];
      if (immutable) {
        field.mutable_ = Immutable;
      }
      return field;
    }

    bool advance() {
      if (!immutable) {
        immutable = true;
        return true;
      }
      immutable = false;
      index = (index + 1) % optionCount;
      return index != 0;
    }
  };

  bool useArray = false;
  std::vector<FieldInfo> fields;

  HeapType operator*() const {
    if (useArray) {
      return Array(fields[0]);
    }
    return Struct(std::vector<Field>(fields.begin(), fields.end()));
  }

  BrandTypeIterator& operator++() {
    for (Index i = fields.size(); i > 0; --i) {
      if (fields[i - 1].advance()) {
        return *this;
      }
    }
    if (useArray) {
      useArray = false;
      return *this;
    }
    fields.emplace_back();
    useArray = fields.size() == 1;
    return *this;
  }
};

// Create an adjacency list with edges from supertype to subtypes.
std::vector<std::vector<Index>>
createSubtypeGraph(const std::vector<HeapType>& types) {
  std::unordered_map<HeapType, Index> indices;
  for (auto type : types) {
    indices.insert({type, indices.size()});
  }

  std::vector<std::vector<Index>> subtypeGraph(types.size());
  for (Index i = 0; i < types.size(); ++i) {
    if (auto super = types[i].getDeclaredSuperType()) {
      if (auto it = indices.find(*super); it != indices.end()) {
        subtypeGraph[it->second].push_back(i);
      }
    }
  }
  return subtypeGraph;
}

struct RecGroupInfo;

// As we iterate through the strongly connected components, we may find
// components that have the same shape. When we find such a collision, we merge
// the groups for the components into a single equivalence class where we track
// how we have disambiguated all such isomorphic groups.
struct GroupClassInfo {
  // If the group has just a single type, record it so we can make sure the
  // brand is not identical to it.
  std::optional<HeapType> singletonType;
  // If we have gone through all the permutations of this group with distinct
  // shapes, we need to add a brand type to continue differentiating different
  // groups in the class.
  std::optional<BrandTypeIterator> brand;
  // An adjacency list giving edges from supertypes to subtypes within the
  // group, using indices into the (non-materialized) canonical shape of this
  // group, offset by 1 iff there is a brand type. Used to ensure that we only
  // find emit permutations that respect the constraint that supertypes must be
  // ordered before subtypes.
  std::vector<std::vector<Index>> subtypeGraph;
  // A generator of valid permutations of the components in this class.
  TopologicalOrders orders;

  // Initialize `subtypeGraph` and `orders` based on the canonical ordering
  // encoded by the group and permutation in `info`.
  static std::vector<std::vector<Index>> initSubtypeGraph(RecGroupInfo& info);
  GroupClassInfo(RecGroupInfo& info);

  void advance() {
    ++orders;
    if (orders == orders.end()) {
      advanceBrand();
    }
  }

  void advanceBrand() {
    if (brand) {
      ++*brand;
    } else {
      brand.emplace();
      // Make room in the subtype graph for the brand type, which goes at the
      // beginning of the canonical order.
      subtypeGraph.insert(subtypeGraph.begin(), {{}});
      // Adjust indices.
      for (Index i = 1; i < subtypeGraph.size(); ++i) {
        for (auto& edge : subtypeGraph[i]) {
          ++edge;
        }
      }
    }
    // Make sure the brand is not the same as the real type.
    if (singletonType &&
        RecGroupShape({**brand}) == RecGroupShape({*singletonType})) {
      ++*brand;
    }
    // Start back at the initial permutation with the new brand.
    orders.~TopologicalOrders();
    new (&orders) TopologicalOrders(subtypeGraph);
  }

  // Permute the types in the given group to match the current configuration in
  // this GroupClassInfo.
  void permute(RecGroupInfo&);
};

// The information we keep for each produced rec group.
struct RecGroupInfo {
  // The sequence of input types to be rewritten into this output group.
  std::vector<HeapType> group;
  // The permutation of the canonical shape for this group's class used to
  // arrive at this group's shape. Used when we later find another strongly
  // connected component with this shape to apply the inverse permutation and
  // get that other component's types into the canonical shape before using a
  // fresh permutation to re-shuffle them into their final shape. Only set for
  // groups belonging to nontrivial equivalence classes.
  std::vector<Index> permutation;
  // Does this group include a brand type that does not correspond to a type in
  // the original module?
  bool hasBrand = false;
  // This group may be the representative group for its nontrivial equivalence
  // class, in which case it holds the necessary extra information used to add
  // new groups to the class.
  std::optional<GroupClassInfo> classInfo;
};

std::vector<std::vector<Index>>
GroupClassInfo::initSubtypeGraph(RecGroupInfo& info) {
  assert(!info.classInfo);
  assert(info.permutation.size() == info.group.size());

  std::vector<HeapType> canonical(info.group.size());
  for (Index i = 0; i < info.group.size(); ++i) {
    canonical[info.permutation[i]] = info.group[i];
  }

  return createSubtypeGraph(canonical);
}

GroupClassInfo::GroupClassInfo(RecGroupInfo& info)
  : singletonType(info.group.size() == 1
                    ? std::optional<HeapType>(info.group[0])
                    : std::nullopt),
    brand(std::nullopt), subtypeGraph(initSubtypeGraph(info)),
    orders(subtypeGraph) {}

void GroupClassInfo::permute(RecGroupInfo& info) {
  assert(info.group.size() == info.permutation.size());
  bool insertingBrand = info.group.size() < subtypeGraph.size();
  // First, un-permute the group to get back to the canonical order, offset by 1
  // if we are newly inserting a brand.
  std::vector<HeapType> canonical(info.group.size() + insertingBrand);
  for (Index i = 0; i < info.group.size(); ++i) {
    canonical[info.permutation[i] + insertingBrand] = info.group[i];
  }
  // Update the brand.
  if (brand) {
    canonical[0] = **brand;
  }
  if (insertingBrand) {
    info.group.resize(info.group.size() + 1);
    info.hasBrand = true;
  }
  // Finally, re-permute with the new permutation..
  info.permutation = *orders;
  for (Index i = 0; i < info.group.size(); ++i) {
    info.group[i] = canonical[info.permutation[i]];
  }
}

std::array<Field, BrandTypeIterator::optionCount>
  BrandTypeIterator::fieldOptions = {{}};

void BrandTypeIterator::initFieldOptions() {
  BrandTypeIterator::fieldOptions = {{
    Field(Field::i8, Mutable),
    Field(Field::i16, Mutable),
    Field(Type::i32, Mutable),
    Field(Type::i64, Mutable),
    Field(Type::f32, Mutable),
    Field(Type::f64, Mutable),
    Field(Type(HeapType::any, Nullable), Mutable),
    Field(Type(HeapType::func, Nullable), Mutable),
    Field(Type(HeapType::ext, Nullable), Mutable),
    Field(Type(HeapType::none, Nullable), Mutable),
    Field(Type(HeapType::nofunc, Nullable), Mutable),
    Field(Type(HeapType::noext, Nullable), Mutable),
    Field(Type(HeapType::any, NonNullable), Mutable),
    Field(Type(HeapType::func, NonNullable), Mutable),
    Field(Type(HeapType::ext, NonNullable), Mutable),
    Field(Type(HeapType::none, NonNullable), Mutable),
    Field(Type(HeapType::nofunc, NonNullable), Mutable),
    Field(Type(HeapType::noext, NonNullable), Mutable),
  }};
}

struct MinimizeRecGroups : Pass {
  // The types we are optimizing and their indices in this list.
  std::vector<HeapType> types;

  // A global ordering on all types, including public types. Used to define a
  // total ordering on rec group shapes.
  std::unordered_map<HeapType, Index> typeIndices;

  // As we process strongly connected components, we will construct output
  // recursion groups here.
  std::vector<RecGroupInfo> groups;

  // For each shape of rec group we have created, its index in `groups`.
  std::unordered_map<RecGroupShape, Index> groupShapeIndices;

  // A sentinel index for public group shapes, which are recorded in
  // `groupShapeIndices` but not in `groups`.
  static constexpr Index PublicGroupIndex = -1;

  // Since we won't store public groups in `groups`, we need this separate place
  // to store their types referred to by their `RecGroupShape`s.
  std::vector<std::vector<HeapType>> publicGroupTypes;

  // When we find that two groups are isomorphic to (i.e. permutations of) each
  // other, we combine their equivalence classes and choose a new class
  // representative to use to disambiguate the groups and any further groups
  // that will join the class.
  DisjointSets equivalenceClasses;

  // When we see a new group, we have to make sure its shape doesn't conflict
  // with the shape of any existing group. If there is a conflict, we need to
  // update the group's shape and try again. Maintain a stack of group indices
  // whose shapes we need to check for uniqueness to avoid deep recursions.
  std::vector<Index> shapesToUpdate;

  void run(Module* module) override {
    // There are no recursion groups to minimize if GC is not enabled.
    if (!module->features.hasGC()) {
      return;
    }

    initBrandOptions();

    auto typeInfo = ModuleUtils::collectHeapTypeInfo(
      *module,
      ModuleUtils::TypeInclusion::AllTypes,
      ModuleUtils::VisibilityHandling::FindVisibility);

    types.reserve(typeInfo.size());

    // We cannot optimize public types, but we do need to make sure we don't
    // generate new groups with the same shape.
    std::unordered_set<RecGroup> publicGroups;
    for (auto& [type, info] : typeInfo) {
      if (info.visibility == ModuleUtils::Visibility::Private) {
        // We can optimize private types.
        types.push_back(type);
        typeIndices.insert({type, typeIndices.size()});
      } else {
        publicGroups.insert(type.getRecGroup());
      }
    }

    publicGroupTypes.reserve(publicGroups.size());
    for (auto group : publicGroups) {
      publicGroupTypes.emplace_back(group.begin(), group.end());
      [[maybe_unused]] auto [_, inserted] = groupShapeIndices.insert(
        {RecGroupShape(publicGroupTypes.back()), PublicGroupIndex});
      assert(inserted);
    }

    // The number of types to optimize is an upper bound on the number of
    // recursion groups we will emit.
    groups.reserve(types.size());

    // Compute the strongly connected components and ensure they form distinct
    // recursion groups.
    for (auto scc : TypeSCCs(types)) {
      [[maybe_unused]] Index index = equivalenceClasses.addSet();
      assert(index == groups.size());
      groups.emplace_back();

      // The SCC is not necessarily topologically sorted to have the supertypes
      // come first. Fix that.
      std::vector<HeapType> sccTypes(scc.begin(), scc.end());
      auto deps = createSubtypeGraph(sccTypes);
      auto permutation = *TopologicalOrders(deps).begin();
      groups.back().group.resize(sccTypes.size());
      for (Index i = 0; i < sccTypes.size(); ++i) {
        groups.back().group[i] = sccTypes[permutation[i]];
      }
      assert(shapesToUpdate.empty());
      shapesToUpdate.push_back(index);
      updateShapes();
    }

    rewriteTypes(*module);
  }

  void initBrandOptions() {
    // Initialize the field options for brand types lazily here to avoid
    // depending on global constructor ordering.
    [[maybe_unused]] static bool fieldsInitialized = []() {
      BrandTypeIterator::initFieldOptions();
      return true;
    }();
  }

  void updateShapes() {
    while (!shapesToUpdate.empty()) {
      auto index = shapesToUpdate.back();
      shapesToUpdate.pop_back();
      updateShape(index);
    }
  }

  void updateShape(Index group) {
    auto [it, inserted] =
      groupShapeIndices.insert({RecGroupShape(groups[group].group), group});
    if (inserted) {
      // This shape was unique. We're done.
      return;
    }

    // We have a conflict. There are seven possibilities:
    //
    //   0. We have a conflict with a public group and...
    //
    //     A. We are trying to insert the next permutation of an existing
    //        equivalence class.
    //
    //     B. We are inserting a new group not yet affiliated with a nontrivial
    //        equivalence class.
    //
    //   1. We are trying to insert the next permutation of an existing
    //      equivalence class and have found that...
    //
    //     A. The next permutation has the same shape as some previous
    //        permutation in the same equivalence class.
    //
    //     B. The next permutation has the same shape as some other group
    //        already in a different nontrivial equivalent class.
    //
    //     C. The next permutation has the same shape as some other group
    //        not yet included in a nontrivial equivalence class.
    //
    //   2. We are inserting a new group not yet affiliated with a
    //      nontrivial equivalence class and have found that...
    //
    //     A. It has the same shape as some group in an existing nontrivial
    //        equivalence class.
    //
    //     B. It has the same shape as some other group also not yet affiliated
    //        with a nontrivial equivalence class, so we will form a new
    //        nontrivial equivalence class.
    //
    // These possibilities are handled in order below.

    auto& groupInfo = groups[group];
    Index groupRep = equivalenceClasses.getRoot(group);

    Index other = it->second;

    if (other == PublicGroupIndex) {
      // The group currently has the same shape as some public group. We can't
      // change the public group, so we need to change the shape of the current
      // group.
      //
      // Case 0A: Since we already have a nontrivial equivalence class, we can
      // try the next permutation to avoid conflict with the public group.
      if (groups[groupRep].classInfo) {
        // We have everything we need to generate the next permutation of this
        // group.
        auto& classInfo = *groups[groupRep].classInfo;
        classInfo.advance();
        classInfo.permute(groupInfo);
        shapesToUpdate.push_back(group);
        return;
      }

      // Case 0B: There is no nontrivial equivalence class yet. Create one so
      // we have all the information we need to try a different permutation.
      assert(group == groupRep);
      groupInfo.permutation = getCanonicalPermutation(groupInfo.group);
      groupInfo.classInfo.emplace(groupInfo);
      groupInfo.classInfo->permute(groupInfo);
      shapesToUpdate.push_back(group);
      return;
    }

    auto& otherInfo = groups[other];
    Index otherRep = equivalenceClasses.getRoot(other);

    // Case 1A: We have found a permutation of this group that has the same
    // shape as a previous permutation of this group. Skip the rest of the
    // permutations, which will also have the same shapes as previous
    // permutations.
    if (groupRep == otherRep) {
      assert(groups[groupRep].classInfo);
      auto& classInfo = *groups[groupRep].classInfo;

      // Move to the next permutation after advancing the type brand to skip
      // further repeated shapes.
      classInfo.advanceBrand();
      classInfo.permute(groupInfo);

      shapesToUpdate.push_back(group);
      return;
    }

    // Case 1B: There is a shape conflict with a group from a different
    // nontrivial equivalence class. Because we canonicalize the shapes whenever
    // we first create a nontrivial equivalence class, this is usually not
    // possible; two equivalence classes with groups of the same shape should
    // actually be the same equivalence class. The exception is when there is a
    // group in each class whose brand matches the base type of the other class.
    // In this case we don't actually want to join the classes; we just want to
    // advance the current group to the next permutation to resolve the
    // conflict.
    if (groups[groupRep].classInfo && groups[otherRep].classInfo) {
      auto& classInfo = *groups[groupRep].classInfo;
      classInfo.advance();
      classInfo.permute(groupInfo);
      shapesToUpdate.push_back(group);
      return;
    }

    // Case 1C: The current group belongs to a nontrivial equivalence class and
    // has the same shape as some other group unaffiliated with a nontrivial
    // equivalence class. Bring that other group into our equivalence class and
    // advance the current group to the next permutation to resolve the
    // conflict.
    if (groups[groupRep].classInfo) {
      auto& classInfo = *groups[groupRep].classInfo;

      [[maybe_unused]] Index unionRep =
        equivalenceClasses.getUnion(groupRep, otherRep);
      assert(groupRep == unionRep);

      // `other` must have the same permutation as `group` because they have the
      // same shape. Advance `group` to the next permutation.
      otherInfo.classInfo = std::nullopt;
      otherInfo.permutation = groupInfo.permutation;
      classInfo.advance();
      classInfo.permute(groupInfo);

      shapesToUpdate.push_back(group);
      return;
    }

    // Case 2A: The shape of the current group is already found in an existing
    // nontrivial equivalence class. Join the class and advance to the next
    // permutation to resolve the conflict.
    if (groups[otherRep].classInfo) {
      auto& classInfo = *groups[otherRep].classInfo;

      [[maybe_unused]] Index unionRep =
        equivalenceClasses.getUnion(otherRep, groupRep);
      assert(otherRep == unionRep);

      // Since we matched shapes with `other`, unapplying its permutation gets
      // us back to the canonical shape, from which we can apply a fresh
      // permutation.
      groupInfo.classInfo = std::nullopt;
      groupInfo.permutation = otherInfo.permutation;
      classInfo.advance();
      classInfo.permute(groupInfo);

      shapesToUpdate.push_back(group);
      return;
    }

    // Case 2B: We need to join two groups with matching shapes into a new
    // nontrivial equivalence class.
    assert(!groups[groupRep].classInfo && !groups[otherRep].classInfo);
    assert(group == groupRep && other == otherRep);

    // We are canonicalizing and reinserting the shape for other, so remove it
    // from the map under its current shape.
    groupShapeIndices.erase(it);

    // Put the types for both groups into the canonical order.
    auto permutation = getCanonicalPermutation(groupInfo.group);
    groupInfo.permutation = otherInfo.permutation = std::move(permutation);

    // Set up `other` to be the representative element of the equivalence class.
    otherInfo.classInfo.emplace(otherInfo);
    auto& classInfo = *otherInfo.classInfo;

    // Update both groups to have the initial valid order. Their shapes still
    // match.
    classInfo.permute(otherInfo);
    classInfo.permute(groupInfo);

    // Insert `other` with its new shape. It may end up being joined to another
    // existing equivalence class, in which case its class info will be cleared
    // and `group` will subsequently be added to that same existing class, or
    // alternatively it may be inserted as the representative element of a new
    // class to which `group` will subsequently be joined.
    shapesToUpdate.push_back(group);
    shapesToUpdate.push_back(other);
  }

  std::vector<Index>
  getCanonicalPermutation(const std::vector<HeapType>& types) {
    // The correctness of this part depends on some interesting properties of
    // strongly connected graphs with ordered, directed edges. A permutation of
    // the vertices in a graph is an isomorphism that produces an isomorphic
    // graph. An automorphism is an isomorphism of a structure onto itself, so
    // an automorphism of a graph is a permutation of the vertices that does not
    // change the label-independent properties of a graph. Permutations can be
    // described in terms of sets of cycles of elements.
    //
    // As an example, consider this strongly-connected recursion group:
    //
    //     (rec
    //       (type $a1 (struct (field (ref $a2) (ref $b1))))
    //       (type $a2 (struct (field (ref $a1) (ref $b2))))
    //       (type $b1 (struct (field (ref $b2) (ref $a1))))
    //       (type $b2 (struct (field (ref $b1) (ref $a2))))
    //     )
    //
    // This group has one nontrivial automorphism: ($a1 $a2) ($b1 $b2). Applying
    // this automorphism gives this recursion group:
    //
    //     (rec
    //       (type $a2 (struct (field (ref $a1) (ref $b2))))
    //       (type $a1 (struct (field (ref $a2) (ref $b1))))
    //       (type $b2 (struct (field (ref $b1) (ref $a2))))
    //       (type $b1 (struct (field (ref $b2) (ref $a1))))
    //     )
    //
    // We can verify that the permutation was an automorphism by checking that
    // the label-independent properties of these two rec groups are the same.
    // Indeed, when we write the adjacency matrices with ordered edges for the
    // two graphs, they both come out to this:
    //
    //     0: _ 0 1 _
    //     1: 0 _ _ 1
    //     2: 1 _ _ 0
    //     3: _ 1 0 _
    //
    // In addition to having the same adjacency matrix, the two recursion groups
    // have the same vertex colorings since all of their types have the same
    // top-level structure. Since the label-independent properties of the
    // recursion groups (i.e. all the properties except the intended type
    // identity at each index) are the same, the permutation that takes one
    // group to the other is an automorphism. These are precisely the
    // permutations that do not change a recursion group's shape, so would not
    // change type identity under WebAssembly's isorecursive type system. In
    // other words, automorphisms cannot help us resolve shape conflicts, so we
    // want to avoid generating them.
    //
    // Theorem 1: All cycles in an automorphism of a strongly-connected
    // recursion group are the same size.
    //
    //     Proof: By contradiction. Assume there are two cycles of different
    //     sizes. Because the group is strongly connected, there is a path from
    //     an element in the smaller of these cycles to an element in the
    //     larger. This path must contain an edge between two vertices in cycles
    //     of different sizes N and M such that N < M. Apply the automorphism N
    //     times. The source of this edge has been cycled back to its original
    //     index, but the destination is not yet back at its original index, so
    //     the edge's destination index is different from its original
    //     destination index and the permutation we applied was not an
    //     automorphism after all.
    //
    // Corollary 1.1: No nontrivial automorphism of an SCC may have a stationary
    // element, since either all the cycles have size 1 and the automorphism is
    // trivial or all the cycles have some other size and there are no
    // stationary elements.
    //
    // Corollary 1.2: No two distinct permutations of an SCC with the same first
    // element (e.g. both with some type $t as the first element) have the same
    // shape, since no nontrivial automorphism can keep the first element
    // stationary while mapping one permutation to the other.
    //
    // Find a canonical ordering of the types in this group. The ordering must
    // be independent of the initial order of the types. To do so, consider the
    // orderings given by visitation order on a tree search rooted at each type
    // in the group. Since the group is strongly connected, a tree search from
    // any of types will visit all types in the group, so it will generate a
    // total and deterministic ordering of the types in the group. We can
    // compare the shapes of each of these orderings to organize the root
    // types and their generated orderings into ordered equivalence classes.
    // These equivalence classes each correspond to a cycle in an automorphism
    // of the graph because their elements are vertices that can all occupy the
    // initial index of the graph without the graph structure changing. We can
    // choose an arbitrary ordering from the least equivalence class as a
    // canonical ordering because all orderings in that class describe the same
    // label-independent graph.
    //
    // Compute the orderings generated by DFS on each type.
    std::unordered_set<HeapType> typeSet(types.begin(), types.end());
    std::vector<std::vector<HeapType>> dfsOrders(types.size());
    for (Index i = 0; i < types.size(); ++i) {
      dfsOrders[i].reserve(types.size());
      std::vector<HeapType> workList;
      workList.push_back(types[i]);
      std::unordered_set<HeapType> seen;
      while (!workList.empty()) {
        auto curr = workList.back();
        workList.pop_back();
        if (!typeSet.count(curr)) {
          continue;
        }
        if (seen.insert(curr).second) {
          dfsOrders[i].push_back(curr);
          auto children = curr.getReferencedHeapTypes();
          workList.insert(workList.end(), children.rbegin(), children.rend());
        }
      }
      assert(dfsOrders[i].size() == types.size());
    }

    // Organize the orderings into equivalence classes, mapping equivalent
    // shapes to lists of automorphically equivalent root types.
    std::map<ComparableRecGroupShape, std::vector<HeapType>> typeClasses;
    for (const auto& order : dfsOrders) {
      ComparableRecGroupShape shape(order, [this](HeapType a, HeapType b) {
        return this->typeIndices.at(a) < this->typeIndices.at(b);
      });
      typeClasses[shape].push_back(order[0]);
    }

    // Choose the initial canonical ordering.
    const auto& leastOrder = typeClasses.begin()->first.types;

    // We want our canonical ordering to have the additional property that it
    // contains one type from each equivalence class before a second type of any
    // equivalence class. Since our utility for enumerating the topological
    // sorts of the canonical order keeps the initial element fixed as long as
    // possible before moving to the next one, by Corollary 1.2 and because
    // permutations that begin with types in different equivalence class cannot
    // have the same shape (otherwise those initial types would be in the same
    // equivalence class), this will maximize the number of distinct shapes the
    // utility emits before starting to emit permutations that have the same
    // shapes as previous permutations. Since all the type equivalence classes
    // are the same size by Theorem 1, we can assemble the final order by
    // striping across the equivalence classes. We determine the order of types
    // taken from each equivalence class by sorting them by order of appearance
    // in the least order, which ensures the final order remains independent of
    // the initial order.
    std::unordered_map<HeapType, Index> indexInLeastOrder;
    for (auto type : leastOrder) {
      indexInLeastOrder.insert({type, indexInLeastOrder.size()});
    }
    auto classSize = typeClasses.begin()->second.size();
    for (auto& [shape, members] : typeClasses) {
      assert(members.size() == classSize);
      std::sort(members.begin(), members.end(), [&](HeapType a, HeapType b) {
        return indexInLeastOrder.at(a) < indexInLeastOrder.at(b);
      });
    }
    std::vector<HeapType> finalOrder;
    finalOrder.reserve(types.size());
    for (Index i = 0; i < classSize; ++i) {
      for (auto& [shape, members] : typeClasses) {
        finalOrder.push_back(members[i]);
      }
    }

    // Now what we actually want is the permutation that takes us from the final
    // canonical order to the original order of the types.
    std::unordered_map<HeapType, Index> indexInFinalOrder;
    for (auto type : finalOrder) {
      indexInFinalOrder.insert({type, indexInFinalOrder.size()});
    }
    std::vector<Index> permutation;
    permutation.reserve(types.size());
    for (auto type : types) {
      permutation.push_back(indexInFinalOrder.at(type));
    }
    return permutation;
  }

  void rewriteTypes(Module& wasm) {
    // Map types to indices in the builder.
    std::unordered_map<HeapType, Index> outputIndices;
    Index i = 0;
    for (const auto& group : groups) {
      for (Index j = 0; j < group.group.size(); ++j) {
        // Skip the brand if it exists.
        if (!group.hasBrand || group.permutation[j] != 0) {
          outputIndices.insert({group.group[j], i});
        }
        ++i;
      }
    }

    // Build the types.
    TypeBuilder builder(i);
    i = 0;
    for (const auto& group : groups) {
      builder.createRecGroup(i, group.group.size());
      for (auto type : group.group) {
        builder[i++].copy(type, [&](HeapType ht) -> HeapType {
          if (auto it = outputIndices.find(ht); it != outputIndices.end()) {
            return builder[it->second];
          }
          return ht;
        });
      }
    }
    auto built = builder.build();
    assert(built);
    auto newTypes = *built;

    // Replace the types in the module.
    std::unordered_map<HeapType, HeapType> oldToNew;
    i = 0;
    for (const auto& group : groups) {
      for (Index j = 0; j < group.group.size(); ++j) {
        // Skip the brand again if it exists.
        if (!group.hasBrand || group.permutation[j] != 0) {
          oldToNew[group.group[j]] = newTypes[i];
        }
        ++i;
      }
    }
    GlobalTypeRewriter rewriter(wasm);
    rewriter.mapTypes(oldToNew);
    rewriter.mapTypeNamesAndIndices(oldToNew);
  }
};

} // anonymous namespace

Pass* createMinimizeRecGroupsPass() { return new MinimizeRecGroups(); }

} // namespace wasm
