summaryrefslogtreecommitdiff
path: root/src/passes/MinimizeRecGroups.cpp
blob: 0faf297f545b5c2cf4ba123ec17523542623d3a7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
/*
 * 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