// Licensing scope: see ../../LICENSE and this directory's LICENSE and NOTICE.
// Copyright 2023 WebAssembly Community Group participants.
// Retained historical upstream notice; current derivation remains unconfirmed.
// Conservative attribution from pinned Binaryen version_130 counterpart:
// Copyright 2016 WebAssembly Community Group participants
// Counterpart: src/passes/DuplicateFunctionElimination.cpp (5d704ad5); origin remains unconfirmed.
// Potentially incorporated Binaryen expression retains Apache-2.0 terms.
// Original Starshine contributions use the scoped MIT grant in ../../LICENSE.
// Modified for Starshine through 2026: implementation and validation use
// Starshine's representations and APIs; this is not an unchanged upstream file.
// Conservative notice retention does not establish copying or independence.

///|
pub fn dfe_summary() -> String {
  "Merge duplicate defined functions, rewrite surviving function references, and compact duplicate function and bounded GC singleton type indices."
}

///|
fn dfe_imported_func_count(mod_ : @lib.Module) -> Int32 {
  let mut count = 0
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) =>
      for import_ in imports {
        match import_ {
          @lib.Import(_, _, @lib.FuncExternType(_)) => count += 1
          _ => ()
        }
      }
    None => ()
  }
  count
}

///|
fn dfe_func_annotation_map(
  mod_ : @lib.Module,
) -> Map[UInt32, Array[@lib.FuncAnnotation]] {
  let by_idx : Map[UInt32, Array[@lib.FuncAnnotation]] = Map([])
  match mod_.func_annotation_sec {
    Some(@lib.FuncAnnotationSec(entries)) =>
      for entry in entries {
        let @lib.FuncAnnotationAssoc(entry_idx, annotations) = entry
        let merged = by_idx.get(entry_idx).unwrap_or([]).copy()
        for annotation in annotations {
          if !merged.contains(annotation) {
            merged.push(annotation)
          }
        }
        by_idx[entry_idx] = merged
      }
    None => ()
  }
  by_idx
}

///|
fn dfe_func_annotations(
  by_idx : Map[UInt32, Array[@lib.FuncAnnotation]],
  abs_idx : UInt32,
) -> Array[@lib.FuncAnnotation] {
  by_idx.get(abs_idx).unwrap_or([])
}

///|
fn dfe_ref_type_is_nullable_funcref(ref_type : @lib.RefType) -> Bool {
  ref_type == @lib.RefType::abs(@lib.AbsHeapType::func()) ||
  ref_type ==
  @lib.RefType::new(true, @lib.HeapType::abs(@lib.AbsHeapType::func()))
}

///|
fn dfe_compactable_elem_expr_func_idx(expr : @lib.Expr) -> @lib.FuncIdx? {
  match expr {
    @lib.Expr([@lib.RefFunc(func_idx)]) => Some(func_idx)
    _ => None
  }
}

///|
fn dfe_canonicalize_elem_kind(kind : @lib.ElemKind) -> @lib.ElemKind {
  match kind {
    @lib.FuncsElemKind(_) => kind
    @lib.FuncExprsElemKind(exprs) => {
      let funcs : Array[@lib.FuncIdx] = []
      for expr in exprs {
        match dfe_compactable_elem_expr_func_idx(expr) {
          Some(func_idx) => funcs.push(func_idx)
          None => return kind
        }
      }
      @lib.ElemKind::funcs(funcs)
    }
    @lib.TypedExprsElemKind(ref_type, exprs) => {
      if !dfe_ref_type_is_nullable_funcref(ref_type) {
        return kind
      }
      let funcs : Array[@lib.FuncIdx] = []
      for expr in exprs {
        match dfe_compactable_elem_expr_func_idx(expr) {
          Some(func_idx) => funcs.push(func_idx)
          None => return kind
        }
      }
      @lib.ElemKind::funcs(funcs)
    }
  }
}

///|
fn dfe_canonicalize_elem_segments(mod_ : @lib.Module) -> @lib.Module {
  match mod_.elem_sec {
    Some(@lib.ElemSec(elems)) => {
      let next : Array[@lib.Elem] = []
      let mut changed = false
      for elem in elems {
        let @lib.Elem(mode, kind) = elem
        let rewritten_kind = dfe_canonicalize_elem_kind(kind)
        if rewritten_kind != kind {
          changed = true
        }
        next.push(@lib.Elem::new(mode, rewritten_kind))
      }
      if changed {
        mod_.with_elem_sec(@lib.ElemSec::new(next))
      } else {
        mod_
      }
    }
    None => mod_
  }
}

///|
fn dfe_strip_name_sec(mod_ : @lib.Module) -> @lib.Module {
  mod_.without_name_sec()
}

///|
fn dfe_supports_simple_type_dedup(rec_types : Array[@lib.RecType]) -> Bool {
  for rec_type in rec_types {
    match rec_type {
      @lib.SingleRecType(subtype) => {
        if subtype.super_types().length() > 0 ||
          subtype.describes_type() is Some(_) ||
          subtype.descriptor_type() is Some(_) {
          return false
        }
        match subtype.get_comptype() {
          @lib.FuncCompType(_, _) => ()
          _ => return false
        }
      }
      @lib.GroupRecType(_) => return false
    }
  }
  true
}

///|
fn dfe_duplicate_simple_type_canonical_map(
  rec_types : Array[@lib.RecType],
) -> Map[UInt32, UInt32] {
  let canonical_map : Map[UInt32, UInt32] = Map([])
  if !dfe_supports_simple_type_dedup(rec_types) {
    return dfe_duplicate_backward_singleton_type_map(rec_types)
  }
  let buckets : @hashmap.HashMap[Int32, Array[Int32]] = @hashmap.new(
    capacity=rec_types.length(),
  )
  let mut old_idx = 0
  while old_idx < rec_types.length() {
    let rec_type = rec_types[old_idx]
    let mut canonical_old_idx : UInt32? = None
    let hash_key = rec_type.hash()
    match buckets.get(hash_key) {
      Some(bucket) => {
        let mut bucket_idx = 0
        while bucket_idx < bucket.length() {
          let prev_idx = bucket[bucket_idx]
          if rec_types[prev_idx] == rec_type {
            canonical_old_idx = Some(prev_idx.reinterpret_as_uint())
            break
          }
          bucket_idx += 1
        }
        if canonical_old_idx is None {
          bucket.push(old_idx)
        }
      }
      None => buckets.set(hash_key, [old_idx])
    }
    match canonical_old_idx {
      Some(canonical_old_idx) =>
        canonical_map[old_idx.reinterpret_as_uint()] = canonical_old_idx
      None => ()
    }
    old_idx += 1
  }
  canonical_map
}

///|
priv struct DfeTypeRewriteCache {
  indices : Map[UInt32, @lib.TypeIdx]
  heaps : Map[UInt32, @lib.HeapType]
}

///|
fn dfe_new_type_rewrite_cache() -> DfeTypeRewriteCache {
  { indices: Map([]), heaps: Map([]), }
}

// One CodeSec remap owns this cache. Its keys are direct final targets;
// only immutable indices and concrete heaps are shared, never IR containers
// or type graphs.

///|
fn dfe_shared_rewritten_type_idx(
  target : UInt32,
  cache : DfeTypeRewriteCache?,
) -> @lib.TypeIdx {
  match cache {
    None => @lib.TypeIdx::new(target)
    Some(cache) =>
      match cache.indices.get(target) {
        Some(idx) => idx
        None => {
          let idx = @lib.TypeIdx::new(target)
          cache.indices[target] = idx
          idx
        }
      }
  }
}

///|
fn dfe_rewrite_type_idx(
  idx : @lib.TypeIdx,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.TypeIdx {
  match idx {
    @lib.TypeIdx(raw) =>
      dfe_shared_rewritten_type_idx(remap.get(raw).unwrap_or(raw), index_cache)
    @lib.RecIdx(_) => idx
  }
}

///|
fn dfe_try_rewrite_type_idx(
  idx : @lib.TypeIdx,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.TypeIdx? {
  match idx {
    @lib.TypeIdx(raw) =>
      match remap.get(raw) {
        Some(next) =>
          if next != raw {
            Some(dfe_shared_rewritten_type_idx(next, index_cache))
          } else {
            None
          }
        None => None
      }
    @lib.RecIdx(_) => None
  }
}

///|
fn dfe_type_idx_is_remapped(
  idx : @lib.TypeIdx,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match idx {
    @lib.TypeIdx(raw) => remap.get(raw) != None
    @lib.RecIdx(_) => false
  }
}

///|
fn dfe_type_metadata_uses_remap(
  metadata : @lib.TypeMetadata,
  remap : Map[UInt32, UInt32],
) -> Bool {
  let { describes, descriptor, .. } = metadata
  match describes {
    Some(idx) => if dfe_type_idx_is_remapped(idx, remap) { return true }
    None => ()
  }
  match descriptor {
    Some(idx) => dfe_type_idx_is_remapped(idx, remap)
    None => false
  }
}

///|
fn dfe_heap_type_uses_remap(
  heap_type : @lib.HeapType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match heap_type {
    @lib.AbsHeapTypeHeapType(_) | @lib.SharedAbsHeapTypeHeapType(_) => false
    @lib.HeapType(type_idx) => dfe_type_idx_is_remapped(type_idx, remap)
    @lib.DefTypeHeapType(@lib.DefType(rec_type, _)) =>
      dfe_rec_type_uses_remap(rec_type, remap)
  }
}

///|
fn dfe_ref_type_uses_remap(
  ref_type : @lib.RefType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match ref_type {
    @lib.HeapTypeRefType(_, _, heap_type) =>
      dfe_heap_type_uses_remap(heap_type, remap)
    @lib.AbsHeapTypeRefType(_) => false
  }
}

///|
fn dfe_val_type_uses_remap(
  val_type : @lib.ValType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match val_type {
    @lib.RefTypeValType(ref_type) => dfe_ref_type_uses_remap(ref_type, remap)
    _ => false
  }
}

///|
fn dfe_storage_type_uses_remap(
  storage_type : @lib.StorageType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match storage_type {
    @lib.ValTypeStorageType(val_type) =>
      dfe_val_type_uses_remap(val_type, remap)
    @lib.PackTypeStorageType(_) => false
  }
}

///|
fn dfe_field_type_uses_remap(
  field_type : @lib.FieldType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  let @lib.FieldType(storage_type, _) = field_type
  dfe_storage_type_uses_remap(storage_type, remap)
}

///|
fn dfe_field_types_use_remap(
  field_types : Array[@lib.FieldType],
  remap : Map[UInt32, UInt32],
) -> Bool {
  let mut idx = 0
  while idx < field_types.length() {
    if dfe_field_type_uses_remap(field_types[idx], remap) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_val_types_use_remap(
  val_types : Array[@lib.ValType],
  remap : Map[UInt32, UInt32],
) -> Bool {
  let mut idx = 0
  while idx < val_types.length() {
    if dfe_val_type_uses_remap(val_types[idx], remap) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_comp_type_uses_remap(
  comp_type : @lib.CompType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match comp_type {
    @lib.ArrayCompType(field_type) =>
      dfe_field_type_uses_remap(field_type, remap)
    @lib.StructCompType(field_types) =>
      dfe_field_types_use_remap(field_types, remap)
    @lib.FuncCompType(params, results) =>
      dfe_val_types_use_remap(params, remap) ||
      dfe_val_types_use_remap(results, remap)
    @lib.ContCompType(func_type) => dfe_type_idx_is_remapped(func_type, remap)
  }
}

///|
fn dfe_type_idxs_use_remap(
  type_idxs : Array[@lib.TypeIdx],
  remap : Map[UInt32, UInt32],
) -> Bool {
  let mut idx = 0
  while idx < type_idxs.length() {
    if dfe_type_idx_is_remapped(type_idxs[idx], remap) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_sub_type_uses_remap(
  subtype : @lib.SubType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match subtype {
    @lib.SubType(_, supers, metadata, comp_type) =>
      dfe_type_idxs_use_remap(supers, remap) ||
      dfe_type_metadata_uses_remap(metadata, remap) ||
      dfe_comp_type_uses_remap(comp_type, remap)
    @lib.CompTypeSubType(metadata, comp_type) =>
      dfe_type_metadata_uses_remap(metadata, remap) ||
      dfe_comp_type_uses_remap(comp_type, remap)
  }
}

///|
fn dfe_sub_types_use_remap(
  subtypes : Array[@lib.SubType],
  remap : Map[UInt32, UInt32],
) -> Bool {
  let mut idx = 0
  while idx < subtypes.length() {
    if dfe_sub_type_uses_remap(subtypes[idx], remap) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_rec_type_uses_remap(
  rec_type : @lib.RecType,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match rec_type {
    @lib.SingleRecType(subtype) => dfe_sub_type_uses_remap(subtype, remap)
    @lib.GroupRecType(subtypes) => dfe_sub_types_use_remap(subtypes, remap)
  }
}

///|
fn dfe_type_metadata_mentions_type_idxs(metadata : @lib.TypeMetadata) -> Bool {
  let { describes, descriptor, .. } = metadata
  describes is Some(_) || descriptor is Some(_)
}

///|
fn dfe_heap_type_mentions_type_idxs(heap_type : @lib.HeapType) -> Bool {
  match heap_type {
    @lib.AbsHeapTypeHeapType(_) | @lib.SharedAbsHeapTypeHeapType(_) => false
    @lib.HeapType(_) => true
    @lib.DefTypeHeapType(@lib.DefType(rec_type, _)) =>
      dfe_rec_type_mentions_type_idxs(rec_type)
  }
}

///|
fn dfe_ref_type_mentions_type_idxs(ref_type : @lib.RefType) -> Bool {
  match ref_type {
    @lib.HeapTypeRefType(_, _, heap_type) =>
      dfe_heap_type_mentions_type_idxs(heap_type)
    @lib.AbsHeapTypeRefType(_) => false
  }
}

///|
fn dfe_val_type_mentions_type_idxs(val_type : @lib.ValType) -> Bool {
  match val_type {
    @lib.RefTypeValType(ref_type) => dfe_ref_type_mentions_type_idxs(ref_type)
    _ => false
  }
}

///|
fn dfe_storage_type_mentions_type_idxs(storage_type : @lib.StorageType) -> Bool {
  match storage_type {
    @lib.ValTypeStorageType(val_type) =>
      dfe_val_type_mentions_type_idxs(val_type)
    @lib.PackTypeStorageType(_) => false
  }
}

///|
fn dfe_field_type_mentions_type_idxs(field_type : @lib.FieldType) -> Bool {
  let @lib.FieldType(storage_type, _) = field_type
  dfe_storage_type_mentions_type_idxs(storage_type)
}

///|
fn dfe_field_types_mention_type_idxs(
  field_types : Array[@lib.FieldType],
) -> Bool {
  let mut idx = 0
  while idx < field_types.length() {
    if dfe_field_type_mentions_type_idxs(field_types[idx]) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_val_types_mention_type_idxs(val_types : Array[@lib.ValType]) -> Bool {
  let mut idx = 0
  while idx < val_types.length() {
    if dfe_val_type_mentions_type_idxs(val_types[idx]) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_type_idxs_mention_type_idxs(type_idxs : Array[@lib.TypeIdx]) -> Bool {
  let mut idx = 0
  while idx < type_idxs.length() {
    match type_idxs[idx] {
      @lib.TypeIdx(_) => return true
      @lib.RecIdx(_) => ()
    }
    idx += 1
  }
  false
}

///|
fn dfe_comp_type_mentions_type_idxs(comp_type : @lib.CompType) -> Bool {
  match comp_type {
    @lib.ArrayCompType(field_type) =>
      dfe_field_type_mentions_type_idxs(field_type)
    @lib.StructCompType(field_types) =>
      dfe_field_types_mention_type_idxs(field_types)
    @lib.FuncCompType(params, results) =>
      dfe_val_types_mention_type_idxs(params) ||
      dfe_val_types_mention_type_idxs(results)
    @lib.ContCompType(func_type) =>
      match func_type {
        @lib.TypeIdx(_) => true
        @lib.RecIdx(_) => false
      }
  }
}

///|
fn dfe_sub_type_mentions_type_idxs(subtype : @lib.SubType) -> Bool {
  match subtype {
    @lib.SubType(_, supers, metadata, comp_type) =>
      dfe_type_idxs_mention_type_idxs(supers) ||
      dfe_type_metadata_mentions_type_idxs(metadata) ||
      dfe_comp_type_mentions_type_idxs(comp_type)
    @lib.CompTypeSubType(metadata, comp_type) =>
      dfe_type_metadata_mentions_type_idxs(metadata) ||
      dfe_comp_type_mentions_type_idxs(comp_type)
  }
}

///|
fn dfe_sub_types_mention_type_idxs(subtypes : Array[@lib.SubType]) -> Bool {
  let mut idx = 0
  while idx < subtypes.length() {
    if dfe_sub_type_mentions_type_idxs(subtypes[idx]) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_rec_type_mentions_type_idxs(rec_type : @lib.RecType) -> Bool {
  match rec_type {
    @lib.SingleRecType(subtype) => dfe_sub_type_mentions_type_idxs(subtype)
    @lib.GroupRecType(subtypes) => dfe_sub_types_mention_type_idxs(subtypes)
  }
}

///|
fn dfe_block_type_mentions_type_idxs(block_type : @lib.BlockType) -> Bool {
  match block_type {
    @lib.VoidBlockType => false
    @lib.ValTypeBlockType(val_type) => dfe_val_type_mentions_type_idxs(val_type)
    @lib.TypeIdxBlockType(_) => true
  }
}

///|
fn dfe_instruction_array_mentions_type_idxs(
  instrs : Array[@lib.Instruction],
) -> Bool {
  let mut idx = 0
  while idx < instrs.length() {
    if dfe_instruction_mentions_type_idxs(instrs[idx]) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_expr_mentions_type_idxs(expr : @lib.Expr) -> Bool {
  let @lib.Expr(instrs) = expr
  dfe_instruction_array_mentions_type_idxs(instrs)
}

///|
fn _dfe_locals_mention_type_idxs(locals : @lib.Locals) -> Bool {
  let runs = locals.runs()
  let mut idx = 0
  while idx < runs.length() {
    if dfe_val_type_mentions_type_idxs(runs[idx].val_type()) {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_instruction_mentions_type_idxs(instr : @lib.Instruction) -> Bool {
  match instr {
    @lib.StructWait(_, _)
    | @lib.ArrayLoad(_, _, _)
    | @lib.ArrayStore(_, _, _)
    | @lib.StructAtomicSet(_, _, _)
    | @lib.StructAtomicRmw(_, _, _, _)
    | @lib.StructAtomicCmpxchg(_, _, _)
    | @lib.ArrayAtomicGet(_, _)
    | @lib.ArrayAtomicGetS(_, _)
    | @lib.ArrayAtomicGetU(_, _)
    | @lib.ArrayAtomicSet(_, _)
    | @lib.ArrayAtomicRmw(_, _, _)
    | @lib.ArrayAtomicCmpxchg(_, _) => true
    @lib.ContNew(_)
    | @lib.ContBind(_, _)
    | @lib.Resume(_, _)
    | @lib.ResumeThrow(_, _, _)
    | @lib.ResumeThrowRef(_, _)
    | @lib.StackSwitch(_, _)
    | @lib.BrOnCastDescEq(_, _, _)
    | @lib.BrOnCastDescEqFail(_, _, _) => true
    @lib.Try(_, _, _, _) =>
      dfe_scan_rewrite_instruction_type_idxs(instr, Map([])).mentions_type_idxs
    @lib.Block(block_type, expr) =>
      dfe_block_type_mentions_type_idxs(block_type) ||
      dfe_expr_mentions_type_idxs(expr)
    @lib.Loop(block_type, expr) =>
      dfe_block_type_mentions_type_idxs(block_type) ||
      dfe_expr_mentions_type_idxs(expr)
    @lib.If(block_type, then_body, else_body) => {
      let else_mentions = match else_body {
        Some(body) => dfe_instruction_array_mentions_type_idxs(body)
        None => false
      }
      dfe_block_type_mentions_type_idxs(block_type) ||
      dfe_instruction_array_mentions_type_idxs(then_body) ||
      else_mentions
    }
    @lib.CallIndirect(_, _) => true
    @lib.ReturnCallIndirect(_, _) => true
    @lib.CallRef(_) => true
    @lib.ReturnCallRef(_) => true
    @lib.Select(Some(types)) => dfe_val_types_mention_type_idxs(types)
    @lib.Select(None) => false
    @lib.TryTable(block_type, _, body) =>
      dfe_block_type_mentions_type_idxs(block_type) ||
      dfe_expr_mentions_type_idxs(body)
    @lib.RefNull(ref_type) => dfe_ref_type_mentions_type_idxs(ref_type)
    @lib.StructNew(_) => true
    @lib.StructNewDefault(_) => true
    @lib.StructNewDesc(_) => true
    @lib.StructNewDefaultDesc(_) => true
    @lib.StructGet(_, _) => true
    @lib.StructGetS(_, _) => true
    @lib.StructGetU(_, _) => true
    @lib.StructAtomicGet(_, _, _) => true
    @lib.StructAtomicGetS(_, _, _) => true
    @lib.StructAtomicGetU(_, _, _) => true
    @lib.StructSet(_, _) => true
    @lib.ArrayNew(_) => true
    @lib.ArrayNewDefault(_) => true
    @lib.ArrayNewFixed(_, _) => true
    @lib.ArrayNewData(_, _) => true
    @lib.ArrayNewElem(_, _) => true
    @lib.ArrayGet(_) => true
    @lib.ArrayGetS(_) => true
    @lib.ArrayGetU(_) => true
    @lib.ArraySet(_) => true
    @lib.ArrayFill(_) => true
    @lib.ArrayCopy(_, _) => true
    @lib.ArrayInitData(_, _) => true
    @lib.ArrayInitElem(_, _) => true
    @lib.RefGetDesc(_) => true
    @lib.RefTest(_, _, heap_type) => dfe_heap_type_mentions_type_idxs(heap_type)
    @lib.RefCast(_, _, heap_type) => dfe_heap_type_mentions_type_idxs(heap_type)
    @lib.RefTestDesc(_, heap_type) =>
      dfe_heap_type_mentions_type_idxs(heap_type)
    @lib.RefCastDescEq(_, _, heap_type) =>
      dfe_heap_type_mentions_type_idxs(heap_type)
    @lib.BrOnCast(_, _, source, target) =>
      dfe_heap_type_mentions_type_idxs(source) ||
      dfe_heap_type_mentions_type_idxs(target)
    @lib.BrOnCastFail(_, _, source, target) =>
      dfe_heap_type_mentions_type_idxs(source) ||
      dfe_heap_type_mentions_type_idxs(target)
    _ => false
  }
}

///|
fn dfe_rewrite_type_metadata(
  metadata : @lib.TypeMetadata,
  remap : Map[UInt32, UInt32],
) -> @lib.TypeMetadata {
  let { describes, descriptor, shared, } = metadata
  @lib.TypeMetadata::new(
    describes=match describes {
      Some(idx) => Some(dfe_rewrite_type_idx(idx, remap))
      None => None
    },
    descriptor=match descriptor {
      Some(idx) => Some(dfe_rewrite_type_idx(idx, remap))
      None => None
    },
    shared~,
  )
}

// The index was already mapped once. Cache only concrete absolute wrappers;
// recursive-group indices and inline/abstract/shared type forms stay separate.

///|
fn dfe_shared_rewritten_heap_type(
  idx : @lib.TypeIdx,
  cache : DfeTypeRewriteCache?,
) -> @lib.HeapType {
  match (idx, cache) {
    (@lib.TypeIdx(target), Some(cache)) =>
      match cache.heaps.get(target) {
        Some(heap_type) => heap_type
        None => {
          let heap_type = @lib.HeapType::new(idx)
          cache.heaps[target] = heap_type
          heap_type
        }
      }
    _ => @lib.HeapType::new(idx)
  }
}

///|
fn dfe_rewrite_heap_type(
  heap_type : @lib.HeapType,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.HeapType {
  match heap_type {
    @lib.AbsHeapTypeHeapType(_) | @lib.SharedAbsHeapTypeHeapType(_) => heap_type
    @lib.HeapType(type_idx) =>
      dfe_shared_rewritten_heap_type(
        dfe_rewrite_type_idx(type_idx, remap, index_cache~),
        index_cache,
      )
    @lib.DefTypeHeapType(@lib.DefType(rec_type, idx)) =>
      @lib.HeapType::def_type(
        @lib.DefType::new(dfe_rewrite_rec_type(rec_type, remap), idx),
      )
  }
}

///|
fn dfe_rewrite_ref_type(
  ref_type : @lib.RefType,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.RefType {
  match ref_type {
    @lib.HeapTypeRefType(nullable, exact, heap_type) =>
      @lib.RefType::new(
        nullable,
        dfe_rewrite_heap_type(heap_type, remap, index_cache~),
        exact~,
      )
    @lib.AbsHeapTypeRefType(_) => ref_type
  }
}

///|
fn dfe_try_rewrite_heap_type(
  heap_type : @lib.HeapType,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.HeapType? {
  if dfe_heap_type_uses_remap(heap_type, remap) {
    Some(dfe_rewrite_heap_type(heap_type, remap, index_cache~))
  } else {
    None
  }
}

///|
fn dfe_try_rewrite_ref_type(
  ref_type : @lib.RefType,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.RefType? {
  if dfe_ref_type_uses_remap(ref_type, remap) {
    Some(dfe_rewrite_ref_type(ref_type, remap, index_cache~))
  } else {
    None
  }
}

///|
fn dfe_rewrite_val_type(
  val_type : @lib.ValType,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.ValType {
  match val_type {
    @lib.RefTypeValType(ref_type) =>
      @lib.ValType::ref_type(
        dfe_rewrite_ref_type(ref_type, remap, index_cache~),
      )
    _ => val_type
  }
}

///|
fn dfe_try_rewrite_val_type(
  val_type : @lib.ValType,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.ValType? {
  if dfe_val_type_uses_remap(val_type, remap) {
    Some(dfe_rewrite_val_type(val_type, remap, index_cache~))
  } else {
    None
  }
}

///|
fn dfe_try_rewrite_val_types(
  val_types : Array[@lib.ValType],
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> Array[@lib.ValType]? {
  let rewritten : Array[@lib.ValType] = Array::new(capacity=val_types.length())
  let mut changed = false
  let mut idx = 0
  while idx < val_types.length() {
    let val_type = val_types[idx]
    match dfe_try_rewrite_val_type(val_type, remap, index_cache~) {
      Some(next) => {
        if !changed {
          let mut prefix = 0
          while prefix < idx {
            rewritten.push(val_types[prefix])
            prefix += 1
          }
          changed = true
        }
        rewritten.push(next)
      }
      None => if changed { rewritten.push(val_type) }
    }
    idx += 1
  }
  if changed {
    Some(rewritten)
  } else {
    None
  }
}

///|
fn dfe_rewrite_storage_type(
  storage_type : @lib.StorageType,
  remap : Map[UInt32, UInt32],
) -> @lib.StorageType {
  match storage_type {
    @lib.ValTypeStorageType(val_type) =>
      @lib.StorageType::val_type(dfe_rewrite_val_type(val_type, remap))
    @lib.PackTypeStorageType(_) => storage_type
  }
}

///|
fn dfe_rewrite_field_type(
  field_type : @lib.FieldType,
  remap : Map[UInt32, UInt32],
) -> @lib.FieldType {
  let @lib.FieldType(storage_type, mutability) = field_type
  @lib.FieldType::new(dfe_rewrite_storage_type(storage_type, remap), mutability)
}

///|
fn dfe_rewrite_result_type(
  values : Array[@lib.ValType],
  remap : Map[UInt32, UInt32],
) -> Array[@lib.ValType] {
  let rewritten : Array[@lib.ValType] = []
  for value in values {
    rewritten.push(dfe_rewrite_val_type(value, remap))
  }
  rewritten
}

///|
fn dfe_rewrite_comp_type(
  comp_type : @lib.CompType,
  remap : Map[UInt32, UInt32],
) -> @lib.CompType {
  match comp_type {
    @lib.ArrayCompType(field_type) =>
      @lib.CompType::array(dfe_rewrite_field_type(field_type, remap))
    @lib.StructCompType(field_types) => {
      let rewritten : Array[@lib.FieldType] = []
      for field_type in field_types {
        rewritten.push(dfe_rewrite_field_type(field_type, remap))
      }
      @lib.CompType::struct_(rewritten)
    }
    @lib.FuncCompType(params, results) =>
      @lib.CompType::func(
        dfe_rewrite_result_type(params, remap),
        dfe_rewrite_result_type(results, remap),
      )
    @lib.ContCompType(func_type) =>
      @lib.CompType::cont(dfe_rewrite_type_idx(func_type, remap))
  }
}

///|
fn dfe_rewrite_sub_type(
  subtype : @lib.SubType,
  remap : Map[UInt32, UInt32],
) -> @lib.SubType {
  match subtype {
    @lib.SubType(is_final, supers, metadata, comp_type) => {
      let rewritten_supers : Array[@lib.TypeIdx] = []
      for super_type in supers {
        rewritten_supers.push(dfe_rewrite_type_idx(super_type, remap))
      }
      @lib.SubType::new(
        is_final,
        rewritten_supers,
        dfe_rewrite_comp_type(comp_type, remap),
        metadata=dfe_rewrite_type_metadata(metadata, remap),
      )
    }
    @lib.CompTypeSubType(metadata, comp_type) =>
      @lib.SubType::comp_type(
        dfe_rewrite_comp_type(comp_type, remap),
        metadata=dfe_rewrite_type_metadata(metadata, remap),
      )
  }
}

///|
fn dfe_rewrite_rec_type(
  rec_type : @lib.RecType,
  remap : Map[UInt32, UInt32],
) -> @lib.RecType {
  match rec_type {
    @lib.SingleRecType(subtype) =>
      @lib.RecType::new(dfe_rewrite_sub_type(subtype, remap))
    @lib.GroupRecType(subtypes) => {
      let rewritten : Array[@lib.SubType] = []
      for subtype in subtypes {
        rewritten.push(dfe_rewrite_sub_type(subtype, remap))
      }
      @lib.RecType::group(rewritten)
    }
  }
}

///|
fn dfe_rewrite_block_type(
  block_type : @lib.BlockType,
  remap : Map[UInt32, UInt32],
) -> @lib.BlockType {
  match block_type {
    @lib.ValTypeBlockType(val_type) =>
      @lib.BlockType::val_type(dfe_rewrite_val_type(val_type, remap))
    @lib.TypeIdxBlockType(type_idx) =>
      @lib.BlockType::type_idx(dfe_rewrite_type_idx(type_idx, remap))
    _ => block_type
  }
}

///|
fn dfe_try_rewrite_block_type(
  block_type : @lib.BlockType,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.BlockType? {
  match block_type {
    @lib.ValTypeBlockType(val_type) =>
      match dfe_try_rewrite_val_type(val_type, remap, index_cache~) {
        Some(next) => Some(@lib.BlockType::val_type(next))
        None => None
      }
    @lib.TypeIdxBlockType(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.BlockType::type_idx(next))
        None => None
      }
    _ => None
  }
}

///|
priv struct DfeTypeRewriteScan[T] {
  rewritten : T?
  mentions_type_idxs : Bool
}

///|
fn dfe_scan_rewrite_val_type(
  val_type : @lib.ValType,
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[@lib.ValType] {
  {
    rewritten: dfe_try_rewrite_val_type(val_type, remap),
    mentions_type_idxs: dfe_val_type_mentions_type_idxs(val_type),
  }
}

///|
fn dfe_scan_rewrite_val_types(
  val_types : Array[@lib.ValType],
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[Array[@lib.ValType]] {
  let rewritten : Array[@lib.ValType] = Array::new(capacity=val_types.length())
  let mut changed = false
  let mut mentions_type_idxs = false
  let mut idx = 0
  while idx < val_types.length() {
    let val_type = val_types[idx]
    let scan = dfe_scan_rewrite_val_type(val_type, remap)
    mentions_type_idxs = mentions_type_idxs || scan.mentions_type_idxs
    match scan.rewritten {
      Some(next) => {
        if !changed {
          let mut prefix = 0
          while prefix < idx {
            rewritten.push(val_types[prefix])
            prefix += 1
          }
          changed = true
        }
        rewritten.push(next)
      }
      None => if changed { rewritten.push(val_type) }
    }
    idx += 1
  }
  {
    rewritten: if changed {
      Some(rewritten)
    } else {
      None
    },
    mentions_type_idxs,
  }
}

///|
fn dfe_scan_rewrite_block_type(
  block_type : @lib.BlockType,
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[@lib.BlockType] {
  match block_type {
    @lib.VoidBlockType => { rewritten: None, mentions_type_idxs: false, }
    @lib.ValTypeBlockType(val_type) => {
      let scan = dfe_scan_rewrite_val_type(val_type, remap)
      {
        rewritten: match scan.rewritten {
          Some(next) => Some(@lib.BlockType::val_type(next))
          None => None
        },
        mentions_type_idxs: scan.mentions_type_idxs,
      }
    }
    @lib.TypeIdxBlockType(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.BlockType::type_idx(next))
          None => None
        },
        mentions_type_idxs: true,
      }
  }
}

///|
fn dfe_scan_rewrite_instruction_array_type_idxs(
  instrs : Array[@lib.Instruction],
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[Array[@lib.Instruction]] {
  let rewritten : Array[@lib.Instruction] = Array::new(capacity=instrs.length())
  let mut changed = false
  let mut mentions_type_idxs = false
  let mut idx = 0
  while idx < instrs.length() {
    let instr = instrs[idx]
    let scan = dfe_scan_rewrite_instruction_type_idxs(instr, remap)
    mentions_type_idxs = mentions_type_idxs || scan.mentions_type_idxs
    match scan.rewritten {
      Some(next) => {
        if !changed {
          let mut prefix = 0
          while prefix < idx {
            rewritten.push(instrs[prefix])
            prefix += 1
          }
          changed = true
        }
        rewritten.push(next)
      }
      None => if changed { rewritten.push(instr) }
    }
    idx += 1
  }
  {
    rewritten: if changed {
      Some(rewritten)
    } else {
      None
    },
    mentions_type_idxs,
  }
}

///|
fn dfe_scan_rewrite_expr_type_idxs(
  expr : @lib.Expr,
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[@lib.Expr] {
  let @lib.Expr(instrs) = expr
  let scan = dfe_scan_rewrite_instruction_array_type_idxs(instrs, remap)
  {
    rewritten: match scan.rewritten {
      Some(next) => Some(@lib.Expr::new(next))
      None => None
    },
    mentions_type_idxs: scan.mentions_type_idxs,
  }
}

///|
fn dfe_scan_rewrite_locals_type_idxs(
  locals : @lib.Locals,
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[@lib.Locals] {
  let runs = locals.runs()
  let rewritten : Array[@lib.LocalRun] = Array::new(capacity=runs.length())
  let mut changed = false
  let mut mentions_type_idxs = false
  let mut idx = 0
  while idx < runs.length() {
    let run = runs[idx]
    let scan = dfe_scan_rewrite_val_type(run.val_type(), remap)
    mentions_type_idxs = mentions_type_idxs || scan.mentions_type_idxs
    match scan.rewritten {
      Some(next) => {
        if !changed {
          let mut prefix = 0
          while prefix < idx {
            rewritten.push(runs[prefix])
            prefix += 1
          }
          changed = true
        }
        rewritten.push(@lib.LocalRun::new(run.count(), next))
      }
      None => if changed { rewritten.push(run) }
    }
    idx += 1
  }
  {
    rewritten: if changed {
      Some(@lib.Locals::new(rewritten))
    } else {
      None
    },
    mentions_type_idxs,
  }
}

///|
fn dfe_scan_rewrite_func_type_idxs(
  func : @lib.Func,
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[@lib.Func] {
  let @lib.Func(locals, body) = func
  let locals_scan = dfe_scan_rewrite_locals_type_idxs(locals, remap)
  let body_scan = dfe_scan_rewrite_expr_type_idxs(body, remap)
  {
    rewritten: if locals_scan.rewritten is None && body_scan.rewritten is None {
      None
    } else {
      Some(
        @lib.Func::new(
          match locals_scan.rewritten {
            Some(next) => next
            None => locals
          },
          match body_scan.rewritten {
            Some(next) => next
            None => body
          },
        ),
      )
    },
    mentions_type_idxs: locals_scan.mentions_type_idxs ||
    body_scan.mentions_type_idxs,
  }
}

///|
fn dfe_try_rewrite_instruction_array_type_idxs(
  instrs : Array[@lib.Instruction],
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> Array[@lib.Instruction]? {
  // Most regions mention no remapped type. Allocate output capacity only at
  // the first actual rewrite, preserving the original prefix and input storage.
  let mut rewritten : Array[@lib.Instruction]? = None
  let mut idx = 0
  while idx < instrs.length() {
    let instr = instrs[idx]
    match dfe_try_rewrite_instruction_type_idxs(instr, remap, index_cache~) {
      Some(next) => {
        let out = match rewritten {
          Some(out) => out
          None => {
            let out : Array[@lib.Instruction] = Array::new(
              capacity=instrs.length(),
            )
            let mut prefix = 0
            while prefix < idx {
              out.push(instrs[prefix])
              prefix += 1
            }
            rewritten = Some(out)
            out
          }
        }
        out.push(next)
      }
      None =>
        match rewritten {
          Some(out) => out.push(instr)
          None => ()
        }
    }
    idx += 1
  }
  rewritten
}

///|
fn dfe_try_rewrite_expr_type_idxs(
  expr : @lib.Expr,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.Expr? {
  let @lib.Expr(instrs) = expr
  match
    dfe_try_rewrite_instruction_array_type_idxs(instrs, remap, index_cache~) {
    Some(rewritten) => Some(@lib.Expr::new(rewritten))
    None => None
  }
}

///|
fn dfe_try_rewrite_locals_type_idxs(
  locals : @lib.Locals,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.Locals? {
  let runs = locals.runs()
  let rewritten : Array[@lib.LocalRun] = Array::new(capacity=runs.length())
  let mut changed = false
  let mut idx = 0
  while idx < runs.length() {
    let run = runs[idx]
    match dfe_try_rewrite_val_type(run.val_type(), remap, index_cache~) {
      Some(next) => {
        if !changed {
          let mut prefix = 0
          while prefix < idx {
            rewritten.push(runs[prefix])
            prefix += 1
          }
          changed = true
        }
        rewritten.push(@lib.LocalRun::new(run.count(), next))
      }
      None => if changed { rewritten.push(run) }
    }
    idx += 1
  }
  if changed {
    Some(@lib.Locals::new(rewritten))
  } else {
    None
  }
}

///|
fn dfe_try_rewrite_func_type_idxs(
  func : @lib.Func,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.Func? {
  let @lib.Func(locals, body) = func
  let rewritten_locals = dfe_try_rewrite_locals_type_idxs(
    locals,
    remap,
    index_cache~,
  )
  let rewritten_body = dfe_try_rewrite_expr_type_idxs(body, remap, index_cache~)
  if rewritten_locals is None && rewritten_body is None {
    None
  } else {
    let next_locals = match rewritten_locals {
      Some(next) => next
      None => locals
    }
    let next_body = match rewritten_body {
      Some(next) => next
      None => body
    }
    Some(@lib.Func::new(next_locals, next_body))
  }
}

///|
fn dfe_rewrite_instruction_type_idxs(
  instr : @lib.Instruction,
  remap : Map[UInt32, UInt32],
) -> @lib.Instruction {
  match instr {
    @lib.StructWait(type_idx, field_idx) =>
      @lib.Instruction::struct_wait(
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.ArrayLoad(op, type_idx, ma) =>
      @lib.Instruction::array_load(
        op,
        dfe_rewrite_type_idx(type_idx, remap),
        memarg=ma,
      )
    @lib.ArrayStore(op, type_idx, ma) =>
      @lib.Instruction::array_store(
        op,
        dfe_rewrite_type_idx(type_idx, remap),
        memarg=ma,
      )
    @lib.ContNew(type_idx) =>
      @lib.Instruction::cont_new(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ContBind(source, target) =>
      @lib.Instruction::cont_bind(
        dfe_rewrite_type_idx(source, remap),
        dfe_rewrite_type_idx(target, remap),
      )
    @lib.Resume(type_idx, handlers) =>
      @lib.Instruction::resume_(dfe_rewrite_type_idx(type_idx, remap), handlers)
    @lib.ResumeThrow(type_idx, tag, handlers) =>
      @lib.Instruction::resume_throw(
        dfe_rewrite_type_idx(type_idx, remap),
        tag,
        handlers,
      )
    @lib.ResumeThrowRef(type_idx, handlers) =>
      @lib.Instruction::resume_throw_ref(
        dfe_rewrite_type_idx(type_idx, remap),
        handlers,
      )
    @lib.StackSwitch(type_idx, tag) =>
      @lib.Instruction::stack_switch(dfe_rewrite_type_idx(type_idx, remap), tag)
    @lib.BrOnCastDescEq(label, source, target) =>
      @lib.Instruction::br_on_cast_desc_eq(
        label,
        dfe_rewrite_ref_type(source, remap),
        dfe_rewrite_ref_type(target, remap),
      )
    @lib.BrOnCastDescEqFail(label, source, target) =>
      @lib.Instruction::br_on_cast_desc_eq_fail(
        label,
        dfe_rewrite_ref_type(source, remap),
        dfe_rewrite_ref_type(target, remap),
      )
    @lib.Try(_, _, _, _) =>
      dfe_scan_rewrite_instruction_type_idxs(instr, remap).rewritten.unwrap_or(
        instr,
      )
    @lib.Block(block_type, expr) =>
      @lib.Instruction::block(
        dfe_rewrite_block_type(block_type, remap),
        dfe_rewrite_expr_type_idxs(expr, remap),
      )
    @lib.Loop(block_type, expr) =>
      @lib.Instruction::loop_(
        dfe_rewrite_block_type(block_type, remap),
        dfe_rewrite_expr_type_idxs(expr, remap),
      )
    @lib.If(block_type, then_body, else_body) => {
      let rewritten_then : Array[@lib.Instruction] = []
      for nested in then_body {
        rewritten_then.push(dfe_rewrite_instruction_type_idxs(nested, remap))
      }
      let rewritten_else = match else_body {
        Some(body) => {
          let rewritten : Array[@lib.Instruction] = []
          for nested in body {
            rewritten.push(dfe_rewrite_instruction_type_idxs(nested, remap))
          }
          Some(rewritten)
        }
        None => None
      }
      @lib.Instruction::if_(
        dfe_rewrite_block_type(block_type, remap),
        rewritten_then,
        rewritten_else,
      )
    }
    @lib.CallIndirect(type_idx, table_idx) =>
      @lib.Instruction::call_indirect(
        dfe_rewrite_type_idx(type_idx, remap),
        table_idx,
      )
    @lib.ReturnCallIndirect(type_idx, table_idx) =>
      @lib.Instruction::return_call_indirect(
        dfe_rewrite_type_idx(type_idx, remap),
        table_idx,
      )
    @lib.CallRef(type_idx) =>
      @lib.Instruction::call_ref(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ReturnCallRef(type_idx) =>
      @lib.Instruction::return_call_ref(dfe_rewrite_type_idx(type_idx, remap))
    @lib.Select(Some(types)) =>
      @lib.Instruction::select(
        types=Some(
          types.map(fn(val_type) { dfe_rewrite_val_type(val_type, remap) }),
        ),
      )
    @lib.TryTable(block_type, catches, body) =>
      @lib.Instruction::try_table(
        dfe_rewrite_block_type(block_type, remap),
        catches,
        dfe_rewrite_expr_type_idxs(body, remap),
      )
    @lib.RefNull(ref_type) =>
      @lib.Instruction::ref_null_type(dfe_rewrite_ref_type(ref_type, remap))
    @lib.StructNew(type_idx) =>
      @lib.Instruction::struct_new(dfe_rewrite_type_idx(type_idx, remap))
    @lib.StructNewDefault(type_idx) =>
      @lib.Instruction::struct_new_default(
        dfe_rewrite_type_idx(type_idx, remap),
      )
    @lib.StructNewDesc(type_idx) =>
      @lib.Instruction::struct_new_desc(dfe_rewrite_type_idx(type_idx, remap))
    @lib.StructNewDefaultDesc(type_idx) =>
      @lib.Instruction::struct_new_default_desc(
        dfe_rewrite_type_idx(type_idx, remap),
      )
    @lib.StructGet(type_idx, field_idx) =>
      @lib.Instruction::struct_get(
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.StructGetS(type_idx, field_idx) =>
      @lib.Instruction::struct_get_s(
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.StructGetU(type_idx, field_idx) =>
      @lib.Instruction::struct_get_u(
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.StructAtomicGet(order, type_idx, field_idx) =>
      @lib.Instruction::struct_atomic_get(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.StructAtomicGetS(order, type_idx, field_idx) =>
      @lib.Instruction::struct_atomic_get_s(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.StructAtomicGetU(order, type_idx, field_idx) =>
      @lib.Instruction::struct_atomic_get_u(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.StructAtomicSet(order, type_idx, field_idx) =>
      @lib.Instruction::struct_atomic_set(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.ArrayAtomicSet(order, type_idx) =>
      @lib.Instruction::array_atomic_set(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
      )
    @lib.ArrayAtomicGet(order, type_idx) =>
      @lib.Instruction::array_atomic_get(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
      )
    @lib.ArrayAtomicGetS(order, type_idx) =>
      @lib.Instruction::array_atomic_get_s(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
      )
    @lib.ArrayAtomicGetU(order, type_idx) =>
      @lib.Instruction::array_atomic_get_u(
        order,
        dfe_rewrite_type_idx(type_idx, remap),
      )
    @lib.ArrayAtomicRmw(order, op, type_idx) =>
      @lib.Instruction::array_atomic_rmw(
        op,
        dfe_rewrite_type_idx(type_idx, remap),
        order~,
      )
    @lib.ArrayAtomicCmpxchg(order, type_idx) =>
      @lib.Instruction::array_atomic_cmpxchg(
        dfe_rewrite_type_idx(type_idx, remap),
        order~,
      )
    @lib.StructAtomicRmw(order, op, type_idx, field_idx) =>
      @lib.Instruction::struct_atomic_rmw(
        op,
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
        order~,
      )
    @lib.StructAtomicCmpxchg(order, type_idx, field_idx) =>
      @lib.Instruction::struct_atomic_cmpxchg(
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
        order~,
      )
    @lib.StructSet(type_idx, field_idx) =>
      @lib.Instruction::struct_set(
        dfe_rewrite_type_idx(type_idx, remap),
        field_idx,
      )
    @lib.ArrayNew(type_idx) =>
      @lib.Instruction::array_new(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ArrayNewDefault(type_idx) =>
      @lib.Instruction::array_new_default(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ArrayNewFixed(type_idx, len) =>
      @lib.Instruction::array_new_fixed(
        dfe_rewrite_type_idx(type_idx, remap),
        len,
      )
    @lib.ArrayNewData(type_idx, data_idx) =>
      @lib.Instruction::array_new_data(
        dfe_rewrite_type_idx(type_idx, remap),
        data_idx,
      )
    @lib.ArrayNewElem(type_idx, elem_idx) =>
      @lib.Instruction::array_new_elem(
        dfe_rewrite_type_idx(type_idx, remap),
        elem_idx,
      )
    @lib.ArrayGet(type_idx) =>
      @lib.Instruction::array_get(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ArrayGetS(type_idx) =>
      @lib.Instruction::array_get_s(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ArrayGetU(type_idx) =>
      @lib.Instruction::array_get_u(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ArraySet(type_idx) =>
      @lib.Instruction::array_set(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ArrayFill(type_idx) =>
      @lib.Instruction::array_fill(dfe_rewrite_type_idx(type_idx, remap))
    @lib.ArrayCopy(src_type, dst_type) =>
      @lib.Instruction::array_copy(
        dfe_rewrite_type_idx(src_type, remap),
        dfe_rewrite_type_idx(dst_type, remap),
      )
    @lib.ArrayInitData(type_idx, data_idx) =>
      @lib.Instruction::array_init_data(
        dfe_rewrite_type_idx(type_idx, remap),
        data_idx,
      )
    @lib.ArrayInitElem(type_idx, elem_idx) =>
      @lib.Instruction::array_init_elem(
        dfe_rewrite_type_idx(type_idx, remap),
        elem_idx,
      )
    @lib.RefGetDesc(type_idx) =>
      @lib.Instruction::ref_get_desc(dfe_rewrite_type_idx(type_idx, remap))
    @lib.RefTest(nullable, exact, heap_type) =>
      @lib.Instruction::ref_test_type(
        @lib.RefType::new(
          nullable,
          dfe_rewrite_heap_type(heap_type, remap),
          exact~,
        ),
      )
    @lib.RefCast(nullable, exact, heap_type) =>
      @lib.Instruction::ref_cast_type(
        @lib.RefType::new(
          nullable,
          dfe_rewrite_heap_type(heap_type, remap),
          exact~,
        ),
      )
    @lib.RefTestDesc(nullable, heap_type) =>
      @lib.Instruction::ref_test_desc(
        nullable,
        dfe_rewrite_heap_type(heap_type, remap),
      )
    @lib.RefCastDescEq(nullable, exact, heap_type) =>
      @lib.Instruction::ref_cast_desc_eq_type(
        @lib.RefType::new(
          nullable,
          dfe_rewrite_heap_type(heap_type, remap),
          exact~,
        ),
      )
    @lib.BrOnCast(label_idx, cast_op, source, target) =>
      @lib.Instruction::br_on_cast(
        label_idx,
        cast_op.source_nullable(),
        dfe_rewrite_heap_type(source, remap),
        cast_op.target_nullable(),
        dfe_rewrite_heap_type(target, remap),
        source_exact=cast_op.source_exact(),
        target_exact=cast_op.target_exact(),
      )
    @lib.BrOnCastFail(label_idx, cast_op, source, target) =>
      @lib.Instruction::br_on_cast_fail(
        label_idx,
        cast_op.source_nullable(),
        dfe_rewrite_heap_type(source, remap),
        cast_op.target_nullable(),
        dfe_rewrite_heap_type(target, remap),
        source_exact=cast_op.source_exact(),
        target_exact=cast_op.target_exact(),
      )
    _ => instr
  }
}

///|
fn dfe_scan_rewrite_instruction_type_idxs(
  instr : @lib.Instruction,
  remap : Map[UInt32, UInt32],
) -> DfeTypeRewriteScan[@lib.Instruction] {
  match instr {
    @lib.StructWait(_, _)
    | @lib.ArrayLoad(_, _, _)
    | @lib.ArrayStore(_, _, _) =>
      {
        rewritten: dfe_try_rewrite_instruction_type_idxs(instr, remap),
        mentions_type_idxs: true,
      }
    @lib.ContNew(_)
    | @lib.ContBind(_, _)
    | @lib.Resume(_, _)
    | @lib.ResumeThrow(_, _, _)
    | @lib.ResumeThrowRef(_, _)
    | @lib.StackSwitch(_, _)
    | @lib.BrOnCastDescEq(_, _, _)
    | @lib.BrOnCastDescEqFail(_, _, _) =>
      {
        rewritten: dfe_try_rewrite_instruction_type_idxs(instr, remap),
        mentions_type_idxs: true,
      }
    @lib.Try(block_type, body, catches, delegate) => {
      let block_scan = dfe_scan_rewrite_block_type(block_type, remap)
      let body_scan = dfe_scan_rewrite_expr_type_idxs(body, remap)
      let mut changed = block_scan.rewritten is Some(_) ||
        body_scan.rewritten is Some(_)
      let mut mentions = block_scan.mentions_type_idxs ||
        body_scan.mentions_type_idxs
      let next_catches = catches.map(fn(catch_) {
        let expr = match catch_ {
          @lib.LegacyCatch(_, expr) | @lib.LegacyCatchAll(expr) => expr
        }
        let scan = dfe_scan_rewrite_expr_type_idxs(expr, remap)
        mentions = mentions || scan.mentions_type_idxs
        match scan.rewritten {
          Some(next) => {
            changed = true
            match catch_ {
              @lib.LegacyCatch(tag, _) => @lib.LegacyCatch::tagged(tag, next)
              @lib.LegacyCatchAll(_) => @lib.LegacyCatch::all(next)
            }
          }
          None => catch_
        }
      })
      {
        mentions_type_idxs: mentions,
        rewritten: if changed {
          Some(
            @lib.Instruction::try_(
              block_scan.rewritten.unwrap_or(block_type),
              body_scan.rewritten.unwrap_or(body),
              next_catches,
              delegate~,
            ),
          )
        } else {
          None
        },
      }
    }
    @lib.Block(block_type, expr) => {
      let block_scan = dfe_scan_rewrite_block_type(block_type, remap)
      let expr_scan = dfe_scan_rewrite_expr_type_idxs(expr, remap)
      {
        rewritten: if block_scan.rewritten is None &&
          expr_scan.rewritten is None {
          None
        } else {
          Some(
            @lib.Instruction::block(
              match block_scan.rewritten {
                Some(next) => next
                None => block_type
              },
              match expr_scan.rewritten {
                Some(next) => next
                None => expr
              },
            ),
          )
        },
        mentions_type_idxs: block_scan.mentions_type_idxs ||
        expr_scan.mentions_type_idxs,
      }
    }
    @lib.Loop(block_type, expr) => {
      let block_scan = dfe_scan_rewrite_block_type(block_type, remap)
      let expr_scan = dfe_scan_rewrite_expr_type_idxs(expr, remap)
      {
        rewritten: if block_scan.rewritten is None &&
          expr_scan.rewritten is None {
          None
        } else {
          Some(
            @lib.Instruction::loop_(
              match block_scan.rewritten {
                Some(next) => next
                None => block_type
              },
              match expr_scan.rewritten {
                Some(next) => next
                None => expr
              },
            ),
          )
        },
        mentions_type_idxs: block_scan.mentions_type_idxs ||
        expr_scan.mentions_type_idxs,
      }
    }
    @lib.If(block_type, then_body, else_body) => {
      let block_scan = dfe_scan_rewrite_block_type(block_type, remap)
      let then_scan = dfe_scan_rewrite_instruction_array_type_idxs(
        then_body, remap,
      )
      let else_scan = match else_body {
        Some(body) => dfe_scan_rewrite_instruction_array_type_idxs(body, remap)
        None => { rewritten: None, mentions_type_idxs: false, }
      }
      {
        rewritten: if block_scan.rewritten is None &&
          then_scan.rewritten is None &&
          else_scan.rewritten is None {
          None
        } else {
          Some(
            @lib.Instruction::if_(
              match block_scan.rewritten {
                Some(next) => next
                None => block_type
              },
              match then_scan.rewritten {
                Some(next) => next
                None => then_body
              },
              match else_body {
                Some(body) =>
                  Some(
                    match else_scan.rewritten {
                      Some(next) => next
                      None => body
                    },
                  )
                None => None
              },
            ),
          )
        },
        mentions_type_idxs: block_scan.mentions_type_idxs ||
        then_scan.mentions_type_idxs ||
        else_scan.mentions_type_idxs,
      }
    }
    @lib.CallIndirect(type_idx, table_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::call_indirect(next, table_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ReturnCallIndirect(type_idx, table_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(@lib.Instruction::return_call_indirect(next, table_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.CallRef(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::call_ref(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ReturnCallRef(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::return_call_ref(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.Select(Some(types)) => {
      let scan = dfe_scan_rewrite_val_types(types, remap)
      {
        rewritten: match scan.rewritten {
          Some(next) => Some(@lib.Instruction::select(types=Some(next)))
          None => None
        },
        mentions_type_idxs: scan.mentions_type_idxs,
      }
    }
    @lib.Select(None) => { rewritten: None, mentions_type_idxs: false, }
    @lib.TryTable(block_type, catches, body) => {
      let block_scan = dfe_scan_rewrite_block_type(block_type, remap)
      let body_scan = dfe_scan_rewrite_expr_type_idxs(body, remap)
      {
        rewritten: if block_scan.rewritten is None &&
          body_scan.rewritten is None {
          None
        } else {
          Some(
            @lib.Instruction::try_table(
              match block_scan.rewritten {
                Some(next) => next
                None => block_type
              },
              catches,
              match body_scan.rewritten {
                Some(next) => next
                None => body
              },
            ),
          )
        },
        mentions_type_idxs: block_scan.mentions_type_idxs ||
        body_scan.mentions_type_idxs,
      }
    }
    @lib.RefNull(ref_type) =>
      {
        rewritten: match dfe_try_rewrite_ref_type(ref_type, remap) {
          Some(next) => Some(@lib.Instruction::ref_null_type(next))
          None => None
        },
        mentions_type_idxs: dfe_ref_type_mentions_type_idxs(ref_type),
      }
    @lib.StructNew(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_new(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructNewDefault(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_new_default(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructNewDesc(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_new_desc(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructNewDefaultDesc(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_new_default_desc(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructGet(type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_get(next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructGetS(type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_get_s(next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructGetU(type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_get_u(next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructAtomicGet(order, type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(@lib.Instruction::struct_atomic_get(order, next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructAtomicGetS(order, type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(@lib.Instruction::struct_atomic_get_s(order, next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructAtomicGetU(order, type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(@lib.Instruction::struct_atomic_get_u(order, next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructAtomicSet(order, type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(@lib.Instruction::struct_atomic_set(order, next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayAtomicSet(order, type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_atomic_set(order, next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayAtomicGet(order, type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_atomic_get(order, next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayAtomicGetS(order, type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_atomic_get_s(order, next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayAtomicGetU(order, type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_atomic_get_u(order, next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayAtomicRmw(order, op, type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(@lib.Instruction::array_atomic_rmw(op, next, order~))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayAtomicCmpxchg(order, type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(@lib.Instruction::array_atomic_cmpxchg(next, order~))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructAtomicRmw(order, op, type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(
              @lib.Instruction::struct_atomic_rmw(op, next, field_idx, order~),
            )
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructAtomicCmpxchg(order, type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) =>
            Some(
              @lib.Instruction::struct_atomic_cmpxchg(next, field_idx, order~),
            )
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.StructSet(type_idx, field_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::struct_set(next, field_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayNew(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_new(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayNewDefault(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_new_default(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayNewFixed(type_idx, len) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_new_fixed(next, len))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayNewData(type_idx, data_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_new_data(next, data_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayNewElem(type_idx, elem_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_new_elem(next, elem_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayGet(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_get(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayGetS(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_get_s(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayGetU(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_get_u(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArraySet(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_set(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayFill(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_fill(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayCopy(src_type, dst_type) =>
      {
        rewritten: match
          (
            dfe_try_rewrite_type_idx(src_type, remap),
            dfe_try_rewrite_type_idx(dst_type, remap),
          ) {
          (Some(next_src), Some(next_dst)) =>
            Some(@lib.Instruction::array_copy(next_src, next_dst))
          (Some(next_src), None) =>
            Some(@lib.Instruction::array_copy(next_src, dst_type))
          (None, Some(next_dst)) =>
            Some(@lib.Instruction::array_copy(src_type, next_dst))
          (None, None) => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayInitData(type_idx, data_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_init_data(next, data_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.ArrayInitElem(type_idx, elem_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::array_init_elem(next, elem_idx))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.RefGetDesc(type_idx) =>
      {
        rewritten: match dfe_try_rewrite_type_idx(type_idx, remap) {
          Some(next) => Some(@lib.Instruction::ref_get_desc(next))
          None => None
        },
        mentions_type_idxs: true,
      }
    @lib.RefTest(nullable, exact, heap_type) =>
      {
        rewritten: match dfe_try_rewrite_heap_type(heap_type, remap) {
          Some(next) =>
            Some(
              @lib.Instruction::ref_test_type(
                @lib.RefType::new(nullable, next, exact~),
              ),
            )
          None => None
        },
        mentions_type_idxs: dfe_heap_type_mentions_type_idxs(heap_type),
      }
    @lib.RefCast(nullable, exact, heap_type) =>
      {
        rewritten: match dfe_try_rewrite_heap_type(heap_type, remap) {
          Some(next) =>
            Some(
              @lib.Instruction::ref_cast_type(
                @lib.RefType::new(nullable, next, exact~),
              ),
            )
          None => None
        },
        mentions_type_idxs: dfe_heap_type_mentions_type_idxs(heap_type),
      }
    @lib.RefTestDesc(nullable, heap_type) =>
      {
        rewritten: match dfe_try_rewrite_heap_type(heap_type, remap) {
          Some(next) => Some(@lib.Instruction::ref_test_desc(nullable, next))
          None => None
        },
        mentions_type_idxs: dfe_heap_type_mentions_type_idxs(heap_type),
      }
    @lib.RefCastDescEq(nullable, exact, heap_type) =>
      {
        rewritten: match dfe_try_rewrite_heap_type(heap_type, remap) {
          Some(next) =>
            Some(
              @lib.Instruction::ref_cast_desc_eq_type(
                @lib.RefType::new(nullable, next, exact~),
              ),
            )
          None => None
        },
        mentions_type_idxs: dfe_heap_type_mentions_type_idxs(heap_type),
      }
    @lib.BrOnCast(label_idx, cast_op, source, target) =>
      {
        rewritten: match
          (
            dfe_try_rewrite_heap_type(source, remap),
            dfe_try_rewrite_heap_type(target, remap),
          ) {
          (Some(next_source), Some(next_target)) =>
            Some(
              @lib.Instruction::br_on_cast(
                label_idx,
                cast_op.source_nullable(),
                next_source,
                cast_op.target_nullable(),
                next_target,
                source_exact=cast_op.source_exact(),
                target_exact=cast_op.target_exact(),
              ),
            )
          (Some(next_source), None) =>
            Some(
              @lib.Instruction::br_on_cast(
                label_idx,
                cast_op.source_nullable(),
                next_source,
                cast_op.target_nullable(),
                target,
                source_exact=cast_op.source_exact(),
                target_exact=cast_op.target_exact(),
              ),
            )
          (None, Some(next_target)) =>
            Some(
              @lib.Instruction::br_on_cast(
                label_idx,
                cast_op.source_nullable(),
                source,
                cast_op.target_nullable(),
                next_target,
                source_exact=cast_op.source_exact(),
                target_exact=cast_op.target_exact(),
              ),
            )
          (None, None) => None
        },
        mentions_type_idxs: dfe_heap_type_mentions_type_idxs(source) ||
        dfe_heap_type_mentions_type_idxs(target),
      }
    @lib.BrOnCastFail(label_idx, cast_op, source, target) =>
      {
        rewritten: match
          (
            dfe_try_rewrite_heap_type(source, remap),
            dfe_try_rewrite_heap_type(target, remap),
          ) {
          (Some(next_source), Some(next_target)) =>
            Some(
              @lib.Instruction::br_on_cast_fail(
                label_idx,
                cast_op.source_nullable(),
                next_source,
                cast_op.target_nullable(),
                next_target,
                source_exact=cast_op.source_exact(),
                target_exact=cast_op.target_exact(),
              ),
            )
          (Some(next_source), None) =>
            Some(
              @lib.Instruction::br_on_cast_fail(
                label_idx,
                cast_op.source_nullable(),
                next_source,
                cast_op.target_nullable(),
                target,
                source_exact=cast_op.source_exact(),
                target_exact=cast_op.target_exact(),
              ),
            )
          (None, Some(next_target)) =>
            Some(
              @lib.Instruction::br_on_cast_fail(
                label_idx,
                cast_op.source_nullable(),
                source,
                cast_op.target_nullable(),
                next_target,
                source_exact=cast_op.source_exact(),
                target_exact=cast_op.target_exact(),
              ),
            )
          (None, None) => None
        },
        mentions_type_idxs: dfe_heap_type_mentions_type_idxs(source) ||
        dfe_heap_type_mentions_type_idxs(target),
      }
    _ => { rewritten: None, mentions_type_idxs: false, }
  }
}

///|
fn dfe_try_rewrite_instruction_type_idxs(
  instr : @lib.Instruction,
  remap : Map[UInt32, UInt32],
  index_cache~ : DfeTypeRewriteCache? = None,
) -> @lib.Instruction? {
  match instr {
    @lib.StructWait(type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_wait(next, field_idx))
        None => None
      }
    @lib.ArrayLoad(op, type_idx, ma) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_load(op, next, memarg=ma))
        None => None
      }
    @lib.ArrayStore(op, type_idx, ma) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_store(op, next, memarg=ma))
        None => None
      }
    @lib.ContNew(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::cont_new(next))
        None => None
      }
    @lib.Resume(type_idx, handlers) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::resume_(next, handlers))
        None => None
      }
    @lib.ResumeThrow(type_idx, tag, handlers) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::resume_throw(next, tag, handlers))
        None => None
      }
    @lib.ResumeThrowRef(type_idx, handlers) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::resume_throw_ref(next, handlers))
        None => None
      }
    @lib.StackSwitch(type_idx, tag) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::stack_switch(next, tag))
        None => None
      }
    @lib.ContBind(source, target) => {
      let next_source = dfe_try_rewrite_type_idx(source, remap, index_cache~)
      let next_target = dfe_try_rewrite_type_idx(target, remap, index_cache~)
      if next_source is None && next_target is None {
        None
      } else {
        Some(
          @lib.Instruction::cont_bind(
            next_source.unwrap_or(source),
            next_target.unwrap_or(target),
          ),
        )
      }
    }
    @lib.BrOnCastDescEq(label, source, target) => {
      let next_source = dfe_try_rewrite_ref_type(source, remap, index_cache~)
      let next_target = dfe_try_rewrite_ref_type(target, remap, index_cache~)
      if next_source is None && next_target is None {
        None
      } else {
        Some(
          @lib.Instruction::br_on_cast_desc_eq(
            label,
            next_source.unwrap_or(source),
            next_target.unwrap_or(target),
          ),
        )
      }
    }
    @lib.BrOnCastDescEqFail(label, source, target) => {
      let next_source = dfe_try_rewrite_ref_type(source, remap, index_cache~)
      let next_target = dfe_try_rewrite_ref_type(target, remap, index_cache~)
      if next_source is None && next_target is None {
        None
      } else {
        Some(
          @lib.Instruction::br_on_cast_desc_eq_fail(
            label,
            next_source.unwrap_or(source),
            next_target.unwrap_or(target),
          ),
        )
      }
    }
    @lib.Try(_, _, _, _) =>
      dfe_scan_rewrite_instruction_type_idxs(instr, remap).rewritten
    @lib.Block(block_type, expr) => {
      let rewritten_block_type = dfe_try_rewrite_block_type(
        block_type,
        remap,
        index_cache~,
      )
      let rewritten_expr = dfe_try_rewrite_expr_type_idxs(
        expr,
        remap,
        index_cache~,
      )
      if rewritten_block_type is None && rewritten_expr is None {
        None
      } else {
        let next_block_type = match rewritten_block_type {
          Some(next) => next
          None => block_type
        }
        let next_expr = match rewritten_expr {
          Some(next) => next
          None => expr
        }
        Some(@lib.Instruction::block(next_block_type, next_expr))
      }
    }
    @lib.Loop(block_type, expr) => {
      let rewritten_block_type = dfe_try_rewrite_block_type(
        block_type,
        remap,
        index_cache~,
      )
      let rewritten_expr = dfe_try_rewrite_expr_type_idxs(
        expr,
        remap,
        index_cache~,
      )
      if rewritten_block_type is None && rewritten_expr is None {
        None
      } else {
        let next_block_type = match rewritten_block_type {
          Some(next) => next
          None => block_type
        }
        let next_expr = match rewritten_expr {
          Some(next) => next
          None => expr
        }
        Some(@lib.Instruction::loop_(next_block_type, next_expr))
      }
    }
    @lib.If(block_type, then_body, else_body) => {
      let rewritten_block_type = dfe_try_rewrite_block_type(
        block_type,
        remap,
        index_cache~,
      )
      let rewritten_then = dfe_try_rewrite_instruction_array_type_idxs(
        then_body,
        remap,
        index_cache~,
      )
      let rewritten_else = match else_body {
        Some(body) =>
          match
            dfe_try_rewrite_instruction_array_type_idxs(
              body,
              remap,
              index_cache~,
            ) {
            Some(next) => Some(Some(next))
            None => None
          }
        None => None
      }
      if rewritten_block_type is None &&
        rewritten_then is None &&
        rewritten_else is None {
        None
      } else {
        let next_block_type = match rewritten_block_type {
          Some(next) => next
          None => block_type
        }
        let next_then = match rewritten_then {
          Some(next) => next
          None => then_body
        }
        let next_else = match rewritten_else {
          Some(next) => next
          None => else_body
        }
        Some(@lib.Instruction::if_(next_block_type, next_then, next_else))
      }
    }
    @lib.CallIndirect(type_idx, table_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::call_indirect(next, table_idx))
        None => None
      }
    @lib.ReturnCallIndirect(type_idx, table_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) =>
          Some(@lib.Instruction::return_call_indirect(next, table_idx))
        None => None
      }
    @lib.CallRef(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::call_ref(next))
        None => None
      }
    @lib.ReturnCallRef(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::return_call_ref(next))
        None => None
      }
    @lib.Select(Some(types)) =>
      match dfe_try_rewrite_val_types(types, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::select(types=Some(next)))
        None => None
      }
    @lib.TryTable(block_type, catches, body) => {
      let rewritten_block_type = dfe_try_rewrite_block_type(
        block_type,
        remap,
        index_cache~,
      )
      let rewritten_body = dfe_try_rewrite_expr_type_idxs(
        body,
        remap,
        index_cache~,
      )
      if rewritten_block_type is None && rewritten_body is None {
        None
      } else {
        let next_block_type = match rewritten_block_type {
          Some(next) => next
          None => block_type
        }
        let next_body = match rewritten_body {
          Some(next) => next
          None => body
        }
        Some(@lib.Instruction::try_table(next_block_type, catches, next_body))
      }
    }
    @lib.RefNull(ref_type) =>
      match dfe_try_rewrite_ref_type(ref_type, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::ref_null_type(next))
        None => None
      }
    @lib.StructNew(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_new(next))
        None => None
      }
    @lib.StructNewDefault(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_new_default(next))
        None => None
      }
    @lib.StructNewDesc(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_new_desc(next))
        None => None
      }
    @lib.StructNewDefaultDesc(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_new_default_desc(next))
        None => None
      }
    @lib.StructGet(type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_get(next, field_idx))
        None => None
      }
    @lib.StructGetS(type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_get_s(next, field_idx))
        None => None
      }
    @lib.StructGetU(type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_get_u(next, field_idx))
        None => None
      }
    @lib.StructAtomicGet(order, type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) =>
          Some(@lib.Instruction::struct_atomic_get(order, next, field_idx))
        None => None
      }
    @lib.StructAtomicGetS(order, type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) =>
          Some(@lib.Instruction::struct_atomic_get_s(order, next, field_idx))
        None => None
      }
    @lib.StructAtomicGetU(order, type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) =>
          Some(@lib.Instruction::struct_atomic_get_u(order, next, field_idx))
        None => None
      }
    @lib.StructAtomicSet(order, type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) =>
          Some(@lib.Instruction::struct_atomic_set(order, next, field_idx))
        None => None
      }
    @lib.ArrayAtomicSet(order, type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_atomic_set(order, next))
        None => None
      }
    @lib.ArrayAtomicGet(order, type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_atomic_get(order, next))
        None => None
      }
    @lib.ArrayAtomicGetS(order, type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_atomic_get_s(order, next))
        None => None
      }
    @lib.ArrayAtomicGetU(order, type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_atomic_get_u(order, next))
        None => None
      }
    @lib.ArrayAtomicRmw(order, op, type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_atomic_rmw(op, next, order~))
        None => None
      }
    @lib.ArrayAtomicCmpxchg(order, type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_atomic_cmpxchg(next, order~))
        None => None
      }
    @lib.StructAtomicRmw(order, op, type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) =>
          Some(@lib.Instruction::struct_atomic_rmw(op, next, field_idx, order~))
        None => None
      }
    @lib.StructAtomicCmpxchg(order, type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) =>
          Some(@lib.Instruction::struct_atomic_cmpxchg(next, field_idx, order~))
        None => None
      }
    @lib.StructSet(type_idx, field_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::struct_set(next, field_idx))
        None => None
      }
    @lib.ArrayNew(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_new(next))
        None => None
      }
    @lib.ArrayNewDefault(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_new_default(next))
        None => None
      }
    @lib.ArrayNewFixed(type_idx, len) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_new_fixed(next, len))
        None => None
      }
    @lib.ArrayNewData(type_idx, data_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_new_data(next, data_idx))
        None => None
      }
    @lib.ArrayNewElem(type_idx, elem_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_new_elem(next, elem_idx))
        None => None
      }
    @lib.ArrayGet(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_get(next))
        None => None
      }
    @lib.ArrayGetS(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_get_s(next))
        None => None
      }
    @lib.ArrayGetU(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_get_u(next))
        None => None
      }
    @lib.ArraySet(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_set(next))
        None => None
      }
    @lib.ArrayFill(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_fill(next))
        None => None
      }
    @lib.ArrayCopy(src_type, dst_type) =>
      match
        (
          dfe_try_rewrite_type_idx(src_type, remap, index_cache~),
          dfe_try_rewrite_type_idx(dst_type, remap, index_cache~),
        ) {
        (Some(next_src), Some(next_dst)) =>
          Some(@lib.Instruction::array_copy(next_src, next_dst))
        (Some(next_src), None) =>
          Some(@lib.Instruction::array_copy(next_src, dst_type))
        (None, Some(next_dst)) =>
          Some(@lib.Instruction::array_copy(src_type, next_dst))
        (None, None) => None
      }
    @lib.ArrayInitData(type_idx, data_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_init_data(next, data_idx))
        None => None
      }
    @lib.ArrayInitElem(type_idx, elem_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::array_init_elem(next, elem_idx))
        None => None
      }
    @lib.RefGetDesc(type_idx) =>
      match dfe_try_rewrite_type_idx(type_idx, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::ref_get_desc(next))
        None => None
      }
    @lib.RefTest(nullable, exact, heap_type) =>
      match dfe_try_rewrite_heap_type(heap_type, remap, index_cache~) {
        Some(next) =>
          Some(
            @lib.Instruction::ref_test_type(
              @lib.RefType::new(nullable, next, exact~),
            ),
          )
        None => None
      }
    @lib.RefCast(nullable, exact, heap_type) =>
      match dfe_try_rewrite_heap_type(heap_type, remap, index_cache~) {
        Some(next) =>
          Some(
            @lib.Instruction::ref_cast_type(
              @lib.RefType::new(nullable, next, exact~),
            ),
          )
        None => None
      }
    @lib.RefTestDesc(nullable, heap_type) =>
      match dfe_try_rewrite_heap_type(heap_type, remap, index_cache~) {
        Some(next) => Some(@lib.Instruction::ref_test_desc(nullable, next))
        None => None
      }
    @lib.RefCastDescEq(nullable, exact, heap_type) =>
      match dfe_try_rewrite_heap_type(heap_type, remap, index_cache~) {
        Some(next) =>
          Some(
            @lib.Instruction::ref_cast_desc_eq_type(
              @lib.RefType::new(nullable, next, exact~),
            ),
          )
        None => None
      }
    @lib.BrOnCast(label_idx, cast_op, source, target) =>
      match
        (
          dfe_try_rewrite_heap_type(source, remap, index_cache~),
          dfe_try_rewrite_heap_type(target, remap, index_cache~),
        ) {
        (Some(next_source), Some(next_target)) =>
          Some(
            @lib.Instruction::br_on_cast(
              label_idx,
              cast_op.source_nullable(),
              next_source,
              cast_op.target_nullable(),
              next_target,
              source_exact=cast_op.source_exact(),
              target_exact=cast_op.target_exact(),
            ),
          )
        (Some(next_source), None) =>
          Some(
            @lib.Instruction::br_on_cast(
              label_idx,
              cast_op.source_nullable(),
              next_source,
              cast_op.target_nullable(),
              target,
              source_exact=cast_op.source_exact(),
              target_exact=cast_op.target_exact(),
            ),
          )
        (None, Some(next_target)) =>
          Some(
            @lib.Instruction::br_on_cast(
              label_idx,
              cast_op.source_nullable(),
              source,
              cast_op.target_nullable(),
              next_target,
              source_exact=cast_op.source_exact(),
              target_exact=cast_op.target_exact(),
            ),
          )
        (None, None) => None
      }
    @lib.BrOnCastFail(label_idx, cast_op, source, target) =>
      match
        (
          dfe_try_rewrite_heap_type(source, remap, index_cache~),
          dfe_try_rewrite_heap_type(target, remap, index_cache~),
        ) {
        (Some(next_source), Some(next_target)) =>
          Some(
            @lib.Instruction::br_on_cast_fail(
              label_idx,
              cast_op.source_nullable(),
              next_source,
              cast_op.target_nullable(),
              next_target,
              source_exact=cast_op.source_exact(),
              target_exact=cast_op.target_exact(),
            ),
          )
        (Some(next_source), None) =>
          Some(
            @lib.Instruction::br_on_cast_fail(
              label_idx,
              cast_op.source_nullable(),
              next_source,
              cast_op.target_nullable(),
              target,
              source_exact=cast_op.source_exact(),
              target_exact=cast_op.target_exact(),
            ),
          )
        (None, Some(next_target)) =>
          Some(
            @lib.Instruction::br_on_cast_fail(
              label_idx,
              cast_op.source_nullable(),
              source,
              cast_op.target_nullable(),
              next_target,
              source_exact=cast_op.source_exact(),
              target_exact=cast_op.target_exact(),
            ),
          )
        (None, None) => None
      }
    _ => None
  }
}

///|
fn dfe_rewrite_expr_type_idxs(
  expr : @lib.Expr,
  remap : Map[UInt32, UInt32],
) -> @lib.Expr {
  let @lib.Expr(instrs) = expr
  let rewritten : Array[@lib.Instruction] = Array::new(capacity=instrs.length())
  for instr in instrs {
    rewritten.push(dfe_rewrite_instruction_type_idxs(instr, remap))
  }
  @lib.Expr::new(rewritten)
}

///|
fn _dfe_rewrite_locals_type_idxs(
  locals : @lib.Locals,
  remap : Map[UInt32, UInt32],
) -> @lib.Locals {
  let runs = locals.runs()
  let rewritten : Array[@lib.LocalRun] = Array::new(capacity=runs.length())
  for run in runs {
    rewritten.push(
      @lib.LocalRun::new(
        run.count(),
        dfe_rewrite_val_type(run.val_type(), remap),
      ),
    )
  }
  @lib.Locals::new(rewritten)
}

///|
fn dfe_rewrite_extern_type(
  extern_type : @lib.ExternType,
  remap : Map[UInt32, UInt32],
) -> @lib.ExternType {
  match extern_type {
    @lib.FuncExternType(type_idx) =>
      @lib.ExternType::func(dfe_rewrite_type_idx(type_idx, remap))
    @lib.TableExternType(table_type) => {
      let @lib.TableType(ref_type, limits) = table_type
      @lib.ExternType::table(
        @lib.TableType::new(dfe_rewrite_ref_type(ref_type, remap), limits),
      )
    }
    @lib.MemExternType(_) => extern_type
    @lib.GlobalExternType(@lib.GlobalType(val_type, mutable_)) =>
      @lib.ExternType::global(
        @lib.GlobalType::new(dfe_rewrite_val_type(val_type, remap), mutable_),
      )
    @lib.TagExternType(@lib.TagType(type_idx)) =>
      @lib.ExternType::tag(
        @lib.TagType::new(dfe_rewrite_type_idx(type_idx, remap)),
      )
  }
}

///|
fn dfe_rewrite_import_type_idxs(
  import_ : @lib.Import,
  remap : Map[UInt32, UInt32],
) -> @lib.Import {
  let @lib.Import(module_name, field_name, extern_type) = import_
  @lib.Import::new(
    module_name,
    field_name,
    dfe_rewrite_extern_type(extern_type, remap),
  )
}

///|
fn dfe_rewrite_table_type_idxs(
  table : @lib.Table,
  remap : Map[UInt32, UInt32],
) -> @lib.Table {
  let @lib.Table(@lib.TableType(ref_type, limits), init) = table
  @lib.Table::new(
    @lib.TableType::new(dfe_rewrite_ref_type(ref_type, remap), limits),
    match init {
      Some(expr) => Some(dfe_rewrite_expr_type_idxs(expr, remap))
      None => None
    },
  )
}

///|
fn dfe_rewrite_global_type_idxs(
  global : @lib.Global,
  remap : Map[UInt32, UInt32],
) -> @lib.Global {
  let @lib.Global(@lib.GlobalType(val_type, mutable_), expr) = global
  @lib.Global::new(
    @lib.GlobalType::new(dfe_rewrite_val_type(val_type, remap), mutable_),
    dfe_rewrite_expr_type_idxs(expr, remap),
  )
}

///|
fn dfe_rewrite_elem_mode_type_idxs(
  mode : @lib.ElemMode,
  remap : Map[UInt32, UInt32],
) -> @lib.ElemMode {
  match mode {
    @lib.Active(table_idx, expr) =>
      @lib.ElemMode::active(table_idx, dfe_rewrite_expr_type_idxs(expr, remap))
    _ => mode
  }
}

///|
fn dfe_rewrite_elem_kind_type_idxs(
  kind : @lib.ElemKind,
  remap : Map[UInt32, UInt32],
) -> @lib.ElemKind {
  match kind {
    @lib.FuncsElemKind(_) => kind
    @lib.FuncExprsElemKind(exprs) => {
      let rewritten : Array[@lib.Expr] = []
      for expr in exprs {
        rewritten.push(dfe_rewrite_expr_type_idxs(expr, remap))
      }
      @lib.ElemKind::func_exprs(rewritten)
    }
    @lib.TypedExprsElemKind(ref_type, exprs) => {
      let rewritten : Array[@lib.Expr] = []
      for expr in exprs {
        rewritten.push(dfe_rewrite_expr_type_idxs(expr, remap))
      }
      @lib.ElemKind::typed_exprs(
        dfe_rewrite_ref_type(ref_type, remap),
        rewritten,
      )
    }
  }
}

///|
fn dfe_rewrite_elem_type_idxs(
  elem : @lib.Elem,
  remap : Map[UInt32, UInt32],
) -> @lib.Elem {
  let @lib.Elem(mode, kind) = elem
  @lib.Elem::new(
    dfe_rewrite_elem_mode_type_idxs(mode, remap),
    dfe_rewrite_elem_kind_type_idxs(kind, remap),
  )
}

///|
fn dfe_rewrite_data_mode_type_idxs(
  mode : @lib.DataMode,
  remap : Map[UInt32, UInt32],
) -> @lib.DataMode {
  match mode {
    @lib.Active(mem_idx, expr) =>
      @lib.DataMode::active(mem_idx, dfe_rewrite_expr_type_idxs(expr, remap))
    @lib.Passive => mode
  }
}

///|
fn dfe_rewrite_data_type_idxs(
  data : @lib.Data,
  remap : Map[UInt32, UInt32],
) -> @lib.Data {
  let @lib.Data(mode, bytes) = data
  @lib.Data::new(dfe_rewrite_data_mode_type_idxs(mode, remap), bytes)
}

///|
fn dfe_rewrite_module_type_idxs(
  mod_ : @lib.Module,
  remap : Map[UInt32, UInt32],
  perf~ : HotPerfSession? = None,
  code_rewrite_flags~ : @bitset.BitSet? = None,
  rewrite_type_sec~ : Bool = true,
) -> @lib.Module {
  let mut rewritten = mod_
  match mod_.type_sec {
    _ if !rewrite_type_sec => ()
    Some(@lib.TypeSec(rec_types)) => {
      let timer = dfe_perf_start_timer_opt(perf, "type-rewrite:type-sec")
      let next : Array[@lib.RecType] = []
      for rec_type in rec_types {
        next.push(dfe_rewrite_rec_type(rec_type, remap))
      }
      rewritten = rewritten.with_type_sec(@lib.TypeSec::new(next))
      dfe_perf_stop_timer_opt(perf, "type-rewrite:type-sec", timer)
    }
    None => ()
  }
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) => {
      let next : Array[@lib.Import] = []
      for import_ in imports {
        next.push(dfe_rewrite_import_type_idxs(import_, remap))
      }
      rewritten = rewritten.with_import_sec(@lib.ImportSec::new(next))
    }
    None => ()
  }
  match mod_.func_sec {
    Some(@lib.FuncSec(type_idxs)) => {
      let timer = dfe_perf_start_timer_opt(perf, "type-rewrite:func-sec")
      let next : Array[@lib.TypeIdx] = []
      for type_idx in type_idxs {
        next.push(dfe_rewrite_type_idx(type_idx, remap))
      }
      rewritten = rewritten.with_func_sec(@lib.FuncSec::new(next))
      dfe_perf_stop_timer_opt(perf, "type-rewrite:func-sec", timer)
    }
    None => ()
  }
  match mod_.table_sec {
    Some(@lib.TableSec(tables)) => {
      let next : Array[@lib.Table] = []
      for table in tables {
        next.push(dfe_rewrite_table_type_idxs(table, remap))
      }
      rewritten = rewritten.with_table_sec(@lib.TableSec::new(next))
    }
    None => ()
  }
  match mod_.tag_sec {
    Some(@lib.TagSec(tags)) => {
      let next : Array[@lib.TagType] = []
      for tag in tags {
        let @lib.TagType(type_idx) = tag
        next.push(@lib.TagType::new(dfe_rewrite_type_idx(type_idx, remap)))
      }
      rewritten = rewritten.with_tag_sec(@lib.TagSec::new(next))
    }
    None => ()
  }
  match mod_.global_sec {
    Some(@lib.GlobalSec(globals)) => {
      let next : Array[@lib.Global] = []
      for global in globals {
        next.push(dfe_rewrite_global_type_idxs(global, remap))
      }
      rewritten = rewritten.with_global_sec(@lib.GlobalSec::new(next))
    }
    None => ()
  }
  match mod_.elem_sec {
    Some(@lib.ElemSec(elems)) => {
      let next : Array[@lib.Elem] = []
      for elem in elems {
        next.push(dfe_rewrite_elem_type_idxs(elem, remap))
      }
      rewritten = rewritten.with_elem_sec(@lib.ElemSec::new(next))
    }
    None => ()
  }
  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => {
      // Share immutable TypeIdx and concrete HeapType boxes in this remap.
      // Both caches are bounded by distinct reached final remap targets;
      // no cache survives in the module or changes surrounding proposal flags.
      let index_cache : DfeTypeRewriteCache? = Some(
        dfe_new_type_rewrite_cache(),
      )
      let timer = dfe_perf_start_timer_opt(perf, "type-rewrite:code-sec")
      let next : Array[@lib.Func] = []
      match code_rewrite_flags {
        Some(flags) if flags.length() == funcs.length() => {
          let mut idx = 0
          while idx < funcs.length() {
            let func = funcs[idx]
            if flags[idx] {
              match dfe_try_rewrite_func_type_idxs(func, remap, index_cache~) {
                Some(rewritten_func) => next.push(rewritten_func)
                None => next.push(func)
              }
            } else {
              next.push(func)
            }
            idx += 1
          }
        }
        _ =>
          for func in funcs {
            match dfe_try_rewrite_func_type_idxs(func, remap, index_cache~) {
              Some(rewritten_func) => next.push(rewritten_func)
              None => next.push(func)
            }
          }
      }
      rewritten = rewritten.with_code_sec(@lib.CodeSec::new(next))
      dfe_perf_stop_timer_opt(perf, "type-rewrite:code-sec", timer)
    }
    None => ()
  }
  match mod_.data_sec {
    Some(@lib.DataSec(datas)) => {
      let next : Array[@lib.Data] = []
      for data in datas {
        next.push(dfe_rewrite_data_type_idxs(data, remap))
      }
      rewritten = rewritten.with_data_sec(@lib.DataSec::new(next))
    }
    None => ()
  }
  rewritten
}

///|
fn dfe_rewrite_func_idx(
  idx : @lib.FuncIdx,
  remap : Array[UInt32],
) -> @lib.FuncIdx {
  let @lib.FuncIdx(raw) = idx
  let next = remap[raw.reinterpret_as_int()]
  if next == raw {
    idx
  } else {
    @lib.FuncIdx::new(next)
  }
}

///|
fn dfe_mark_identity_visible_func(
  func_idx : @lib.FuncIdx,
  imported_count : Int32,
  identity_visible : Array[Bool],
) -> Unit {
  let @lib.FuncIdx(raw) = func_idx
  let local_idx = raw.reinterpret_as_int() - imported_count
  if local_idx >= 0 && local_idx < identity_visible.length() {
    identity_visible[local_idx] = true
  }
}

///|
fn dfe_mark_ref_func_identities_in_instruction(
  instr : @lib.Instruction,
  imported_count : Int32,
  identity_visible : Array[Bool],
) -> Unit {
  match instr {
    @lib.Block(_, expr) | @lib.Loop(_, expr) | @lib.TryTable(_, _, expr) =>
      dfe_mark_ref_func_identities_in_expr(
        expr, imported_count, identity_visible,
      )
    @lib.Try(_, body, catches, _) => {
      dfe_mark_ref_func_identities_in_expr(
        body, imported_count, identity_visible,
      )
      for catch_ in catches {
        let expr = match catch_ {
          @lib.LegacyCatch(_, expr) | @lib.LegacyCatchAll(expr) => expr
        }
        dfe_mark_ref_func_identities_in_expr(
          expr, imported_count, identity_visible,
        )
      }
    }
    @lib.If(_, then_body, else_body) => {
      dfe_mark_ref_func_identities_in_instructions(
        then_body, imported_count, identity_visible,
      )
      match else_body {
        Some(body) =>
          dfe_mark_ref_func_identities_in_instructions(
            body, imported_count, identity_visible,
          )
        None => ()
      }
    }
    @lib.RefFunc(func_idx) =>
      dfe_mark_identity_visible_func(func_idx, imported_count, identity_visible)
    _ => ()
  }
}

///|
fn dfe_mark_ref_func_identities_in_instructions(
  instrs : Array[@lib.Instruction],
  imported_count : Int32,
  identity_visible : Array[Bool],
) -> Unit {
  for instr in instrs {
    dfe_mark_ref_func_identities_in_instruction(
      instr, imported_count, identity_visible,
    )
  }
}

///|
fn dfe_mark_ref_func_identities_in_expr(
  expr : @lib.Expr,
  imported_count : Int32,
  identity_visible : Array[Bool],
) -> Unit {
  let @lib.Expr(instrs) = expr
  dfe_mark_ref_func_identities_in_instructions(
    instrs, imported_count, identity_visible,
  )
}

///|
fn dfe_mark_elem_kind_identities(
  kind : @lib.ElemKind,
  imported_count : Int32,
  identity_visible : Array[Bool],
) -> Unit {
  match kind {
    @lib.FuncsElemKind(funcs) =>
      for func_idx in funcs {
        dfe_mark_identity_visible_func(
          func_idx, imported_count, identity_visible,
        )
      }
    @lib.FuncExprsElemKind(exprs) | @lib.TypedExprsElemKind(_, exprs) =>
      for expr in exprs {
        dfe_mark_ref_func_identities_in_expr(
          expr, imported_count, identity_visible,
        )
      }
  }
}

///|
fn dfe_identity_visible_defined_funcs(
  mod_ : @lib.Module,
  imported_count : Int32,
  defined_count : Int32,
) -> Array[Bool] {
  let identity_visible = Array::make(defined_count, false)
  match mod_.export_sec {
    Some(@lib.ExportSec(exports)) =>
      for export_ in exports {
        match export_ {
          @lib.Export(_, @lib.FuncExternIdx(func_idx)) =>
            dfe_mark_identity_visible_func(
              func_idx, imported_count, identity_visible,
            )
          _ => ()
        }
      }
    None => ()
  }
  match mod_.elem_sec {
    Some(@lib.ElemSec(elems)) =>
      for elem in elems {
        let @lib.Elem(mode, kind) = elem
        // Declarative segments only declare ref.func operands for validation;
        // they never instantiate function references at runtime.
        if !(mode is @lib.Declarative) {
          dfe_mark_elem_kind_identities(kind, imported_count, identity_visible)
        }
      }
    None => ()
  }
  match mod_.table_sec {
    Some(@lib.TableSec(tables)) =>
      for table in tables {
        let @lib.Table(_, init) = table
        match init {
          Some(expr) =>
            dfe_mark_ref_func_identities_in_expr(
              expr, imported_count, identity_visible,
            )
          None => ()
        }
      }
    None => ()
  }
  match mod_.global_sec {
    Some(@lib.GlobalSec(globals)) =>
      for global in globals {
        let @lib.Global(_, init) = global
        dfe_mark_ref_func_identities_in_expr(
          init, imported_count, identity_visible,
        )
      }
    None => ()
  }
  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) =>
      for func in funcs {
        let @lib.Func(_, body) = func
        dfe_mark_ref_func_identities_in_expr(
          body, imported_count, identity_visible,
        )
      }
    None => ()
  }
  identity_visible
}

///|
fn dfe_rewrite_instruction_func_idxs(
  instr : @lib.Instruction,
  remap : Array[UInt32],
) -> @lib.Instruction {
  match instr {
    @lib.Block(block_type, expr) => {
      let rewritten = dfe_rewrite_expr_func_idxs(expr, remap)
      if physical_equal(rewritten, expr) {
        instr
      } else {
        @lib.Instruction::block(block_type, rewritten)
      }
    }
    @lib.Loop(block_type, expr) => {
      let rewritten = dfe_rewrite_expr_func_idxs(expr, remap)
      if physical_equal(rewritten, expr) {
        instr
      } else {
        @lib.Instruction::loop_(block_type, rewritten)
      }
    }
    @lib.If(block_type, then_body, else_body) => {
      let rewritten_then = dfe_rewrite_instrs_func_idxs(then_body, remap)
      let mut else_changed = false
      let rewritten_else = match else_body {
        Some(body) => {
          let rewritten = dfe_rewrite_instrs_func_idxs(body, remap)
          else_changed = !physical_equal(rewritten, body)
          Some(rewritten)
        }
        None => None
      }
      if physical_equal(rewritten_then, then_body) && !else_changed {
        instr
      } else {
        @lib.Instruction::if_(block_type, rewritten_then, rewritten_else)
      }
    }
    @lib.Call(func_idx) => {
      let rewritten = dfe_rewrite_func_idx(func_idx, remap)
      if rewritten == func_idx {
        instr
      } else {
        @lib.Instruction::call(rewritten)
      }
    }
    @lib.ReturnCall(func_idx) => {
      let rewritten = dfe_rewrite_func_idx(func_idx, remap)
      if rewritten == func_idx {
        instr
      } else {
        @lib.Instruction::return_call(rewritten)
      }
    }
    @lib.TryTable(block_type, catches, body) => {
      let rewritten = dfe_rewrite_expr_func_idxs(body, remap)
      if physical_equal(rewritten, body) {
        instr
      } else {
        @lib.Instruction::try_table(block_type, catches, rewritten)
      }
    }
    @lib.Try(block_type, body, catches, delegate) => {
      let next_body = dfe_rewrite_expr_func_idxs(body, remap)
      let mut next_catches : Array[@lib.LegacyCatch]? = None
      for idx = 0; idx < catches.length(); idx = idx + 1 {
        let catch_ = catches[idx]
        let expr = match catch_ {
          @lib.LegacyCatch(_, expr) | @lib.LegacyCatchAll(expr) => expr
        }
        let next_expr = dfe_rewrite_expr_func_idxs(expr, remap)
        if physical_equal(next_expr, expr) {
          continue
        }
        let next = match catch_ {
          @lib.LegacyCatch(tag, _) => @lib.LegacyCatch::tagged(tag, next_expr)
          @lib.LegacyCatchAll(_) => @lib.LegacyCatch::all(next_expr)
        }
        match next_catches {
          Some(copy) => copy[idx] = next
          None => {
            let copy = catches.copy()
            copy[idx] = next
            next_catches = Some(copy)
          }
        }
      }
      if physical_equal(next_body, body) && next_catches is None {
        instr
      } else {
        @lib.Instruction::try_(
          block_type,
          next_body,
          next_catches.unwrap_or(catches),
          delegate~,
        )
      }
    }
    @lib.RefFunc(func_idx) => {
      let rewritten = dfe_rewrite_func_idx(func_idx, remap)
      if rewritten == func_idx {
        instr
      } else {
        @lib.Instruction::ref_func(rewritten)
      }
    }
    _ => instr
  }
}

///|
fn dfe_rewrite_instrs_func_idxs(
  instrs : Array[@lib.Instruction],
  remap : Array[UInt32],
) -> Array[@lib.Instruction] {
  let mut out : Array[@lib.Instruction]? = None
  for index, instr in instrs {
    let rewritten = dfe_rewrite_instruction_func_idxs(instr, remap)
    if out is None && !physical_equal(rewritten, instr) {
      let prefix : Array[@lib.Instruction] = Array::new(
        capacity=instrs.length(),
      )
      for offset = 0; offset < index; offset = offset + 1 {
        prefix.push(instrs[offset])
      }
      out = Some(prefix)
    }
    if out is Some(next) {
      next.push(rewritten)
    }
  }
  out.unwrap_or(instrs)
}

///|
fn dfe_rewrite_expr_func_idxs(
  expr : @lib.Expr,
  remap : Array[UInt32],
) -> @lib.Expr {
  let @lib.Expr(instrs) = expr
  let rewritten = dfe_rewrite_instrs_func_idxs(instrs, remap)
  if physical_equal(rewritten, instrs) {
    expr
  } else {
    @lib.Expr::new(rewritten)
  }
}

///|
fn dfe_rewrite_func_func_idxs(
  func : @lib.Func,
  remap : Array[UInt32],
) -> @lib.Func {
  let @lib.Func(locals, body) = func
  let rewritten = dfe_rewrite_expr_func_idxs(body, remap)
  if physical_equal(rewritten, body) {
    func
  } else {
    @lib.Func::new(locals, rewritten)
  }
}

///|
fn dfe_rewrite_extern_idx_func_idxs(
  idx : @lib.ExternIdx,
  remap : Array[UInt32],
) -> @lib.ExternIdx {
  match idx {
    @lib.FuncExternIdx(func_idx) =>
      @lib.ExternIdx::func(dfe_rewrite_func_idx(func_idx, remap))
    _ => idx
  }
}

///|
fn dfe_rewrite_export_func_idxs(
  export_ : @lib.Export,
  remap : Array[UInt32],
) -> @lib.Export {
  let @lib.Export(name, idx) = export_
  @lib.Export::new(name, dfe_rewrite_extern_idx_func_idxs(idx, remap))
}

///|
fn dfe_rewrite_elem_kind_func_idxs(
  kind : @lib.ElemKind,
  remap : Array[UInt32],
) -> @lib.ElemKind {
  match kind {
    @lib.FuncsElemKind(funcs) => {
      let rewritten : Array[@lib.FuncIdx] = Array::new(capacity=funcs.length())
      for func_idx in funcs {
        rewritten.push(dfe_rewrite_func_idx(func_idx, remap))
      }
      @lib.ElemKind::funcs(rewritten)
    }
    @lib.FuncExprsElemKind(exprs) => {
      let rewritten : Array[@lib.Expr] = Array::new(capacity=exprs.length())
      for expr in exprs {
        rewritten.push(dfe_rewrite_expr_func_idxs(expr, remap))
      }
      @lib.ElemKind::func_exprs(rewritten)
    }
    @lib.TypedExprsElemKind(ref_type, exprs) => {
      let rewritten : Array[@lib.Expr] = Array::new(capacity=exprs.length())
      for expr in exprs {
        rewritten.push(dfe_rewrite_expr_func_idxs(expr, remap))
      }
      @lib.ElemKind::typed_exprs(ref_type, rewritten)
    }
  }
}

///|
fn dfe_rewrite_func_annotation_sec(
  sec : @lib.FuncAnnotationSec,
  remap : Array[UInt32],
  drop_unmapped : Bool,
) -> @lib.FuncAnnotationSec {
  let @lib.FuncAnnotationSec(entries) = sec
  let rewritten_indices : Array[UInt32] = []
  let rewritten_annotations : Array[Array[@lib.FuncAnnotation]] = []
  for entry in entries {
    let @lib.FuncAnnotationAssoc(idx, annotations) = entry
    let next_idx = match remap.get(idx.reinterpret_as_int()) {
      Some(next_idx) => next_idx
      None => if drop_unmapped { continue } else { idx }
    }
    let mut slot = -1
    for candidate = 0
        candidate < rewritten_indices.length()
        candidate = candidate + 1 {
      if rewritten_indices[candidate] == next_idx {
        slot = candidate
        break
      }
    }
    if slot < 0 {
      rewritten_indices.push(next_idx)
      rewritten_annotations.push([])
      slot = rewritten_indices.length() - 1
    }
    for annotation in annotations {
      if !rewritten_annotations[slot].contains(annotation) {
        rewritten_annotations[slot].push(annotation)
      }
    }
  }
  let rewritten : Array[@lib.FuncAnnotationAssoc] = []
  for slot = 0; slot < rewritten_indices.length(); slot = slot + 1 {
    rewritten.push(
      @lib.FuncAnnotationAssoc::new(
        rewritten_indices[slot],
        rewritten_annotations[slot],
      ),
    )
  }
  @lib.FuncAnnotationSec::new(rewritten)
}

///|
fn dfe_rewrite_func_annotation_sec_in_module(
  mod_ : @lib.Module,
  remap : Array[UInt32],
  drop_unmapped : Bool,
) -> @lib.Module {
  match mod_.func_annotation_sec {
    Some(sec) =>
      mod_.with_func_annotation_sec(
        dfe_rewrite_func_annotation_sec(sec, remap, drop_unmapped),
      )
    None => mod_
  }
}

///|
fn dfe_rewrite_module_func_idxs(
  mod_ : @lib.Module,
  remap : Array[UInt32],
  perf~ : HotPerfSession? = None,
  code_rewrite_flags~ : @bitset.BitSet? = None,
) -> @lib.Module {
  let mut rewritten = mod_
  match mod_.export_sec {
    Some(@lib.ExportSec(exports)) => {
      let next : Array[@lib.Export] = Array::new(capacity=exports.length())
      for export_ in exports {
        next.push(dfe_rewrite_export_func_idxs(export_, remap))
      }
      rewritten = rewritten.with_export_sec(@lib.ExportSec::new(next))
    }
    None => ()
  }
  match mod_.start_sec {
    Some(@lib.StartSec(func_idx)) =>
      rewritten = rewritten.with_start_sec(
        @lib.StartSec::new(dfe_rewrite_func_idx(func_idx, remap)),
      )
    None => ()
  }
  match mod_.elem_sec {
    Some(@lib.ElemSec(elems)) => {
      let next : Array[@lib.Elem] = Array::new(capacity=elems.length())
      for elem in elems {
        let @lib.Elem(mode, kind) = elem
        next.push(
          @lib.Elem::new(
            match mode {
              @lib.Active(table_idx, expr) =>
                @lib.ElemMode::active(
                  table_idx,
                  dfe_rewrite_expr_func_idxs(expr, remap),
                )
              _ => mode
            },
            dfe_rewrite_elem_kind_func_idxs(kind, remap),
          ),
        )
      }
      rewritten = rewritten.with_elem_sec(@lib.ElemSec::new(next))
    }
    None => ()
  }
  match mod_.table_sec {
    Some(@lib.TableSec(tables)) => {
      let next : Array[@lib.Table] = Array::new(capacity=tables.length())
      for table in tables {
        let @lib.Table(table_type, init) = table
        next.push(
          @lib.Table::new(
            table_type,
            match init {
              Some(expr) => Some(dfe_rewrite_expr_func_idxs(expr, remap))
              None => None
            },
          ),
        )
      }
      rewritten = rewritten.with_table_sec(@lib.TableSec::new(next))
    }
    None => ()
  }
  match mod_.global_sec {
    Some(@lib.GlobalSec(globals)) => {
      let next : Array[@lib.Global] = Array::new(capacity=globals.length())
      for global in globals {
        let @lib.Global(global_type, expr) = global
        next.push(
          @lib.Global::new(global_type, dfe_rewrite_expr_func_idxs(expr, remap)),
        )
      }
      rewritten = rewritten.with_global_sec(@lib.GlobalSec::new(next))
    }
    None => ()
  }
  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => {
      let timer = dfe_perf_start_timer_opt(perf, "func-rewrite:code-sec")
      let next : Array[@lib.Func] = Array::new(capacity=funcs.length())
      match code_rewrite_flags {
        Some(flags) if flags.length() == funcs.length() =>
          for idx = 0; idx < funcs.length(); idx = idx + 1 {
            if flags[idx] {
              next.push(dfe_rewrite_func_func_idxs(funcs[idx], remap))
            } else {
              next.push(funcs[idx])
            }
          }
        _ =>
          for func in funcs {
            next.push(dfe_rewrite_func_func_idxs(func, remap))
          }
      }
      rewritten = rewritten.with_code_sec(@lib.CodeSec::new(next))
      dfe_perf_stop_timer_opt(perf, "func-rewrite:code-sec", timer)
    }
    None => ()
  }
  match mod_.data_sec {
    Some(@lib.DataSec(datas)) => {
      let next : Array[@lib.Data] = Array::new(capacity=datas.length())
      for data in datas {
        let @lib.Data(mode, bytes) = data
        next.push(
          @lib.Data::new(
            match mode {
              @lib.Active(mem_idx, expr) =>
                @lib.DataMode::active(
                  mem_idx,
                  dfe_rewrite_expr_func_idxs(expr, remap),
                )
              @lib.Passive => mode
            },
            bytes,
          ),
        )
      }
      rewritten = rewritten.with_data_sec(@lib.DataSec::new(next))
    }
    None => ()
  }
  dfe_rewrite_func_annotation_sec_in_module(rewritten, remap, false)
}

///|
fn dfe_canonical_remap_sources(
  remap : Map[UInt32, UInt32],
) -> Map[UInt32, UInt32] {
  let canonical : Map[UInt32, UInt32] = Map([])
  for source_idx in remap.keys() {
    let target_idx = remap[source_idx]
    match canonical.get(target_idx) {
      Some(existing) =>
        if source_idx < existing {
          canonical[target_idx] = source_idx
        }
      None => canonical[target_idx] = source_idx
    }
  }
  canonical
}

///|
fn dfe_rewrite_name_map_filtered(
  names : @lib.NameMap,
  remap : Map[UInt32, UInt32],
  drop_unmapped : Bool,
) -> @lib.NameMap {
  let @lib.NameMap(entries) = names
  let canonical_sources = dfe_canonical_remap_sources(remap)
  let rewritten : Array[@lib.NameAssoc] = []
  let mut last_idx : UInt32? = None
  for entry in entries {
    let @lib.NameAssoc(idx, name) = entry
    match remap.get(idx) {
      Some(next_idx) =>
        if canonical_sources.get(next_idx) == Some(idx) &&
          last_idx != Some(next_idx) {
          rewritten.push(@lib.NameAssoc::new(next_idx, name))
          last_idx = Some(next_idx)
        }
      None =>
        if !drop_unmapped && last_idx != Some(idx) {
          rewritten.push(entry)
          last_idx = Some(idx)
        }
    }
  }
  @lib.NameMap::new(rewritten)
}

///|
fn _dfe_rewrite_local_name_map_filtered(
  names : @lib.IndirectNameMap,
  remap : Map[UInt32, UInt32],
  drop_unmapped : Bool,
) -> @lib.IndirectNameMap {
  let @lib.IndirectNameMap(entries) = names
  let canonical_sources = dfe_canonical_remap_sources(remap)
  let rewritten : Array[@lib.IndirectNameAssoc] = []
  let mut last_idx : UInt32? = None
  for entry in entries {
    let @lib.IndirectNameAssoc(idx, local_names) = entry
    match remap.get(idx) {
      Some(next_idx) =>
        if canonical_sources.get(next_idx) == Some(idx) &&
          last_idx != Some(next_idx) {
          rewritten.push(@lib.IndirectNameAssoc::new(next_idx, local_names))
          last_idx = Some(next_idx)
        }
      None =>
        if !drop_unmapped && last_idx != Some(idx) {
          rewritten.push(entry)
          last_idx = Some(idx)
        }
    }
  }
  @lib.IndirectNameMap::new(rewritten)
}

///|
fn dfe_rewrite_type_name_sec(
  name_sec : @lib.NameSec,
  type_remap : Map[UInt32, UInt32],
) -> @lib.NameSec {
  let {
    module_name,
    func_names,
    local_names,
    label_names,
    type_names,
    table_names,
    memory_names,
    global_names,
    elem_names,
    data_names,
    field_names,
    tag_names,
  } = name_sec
  @lib.NameSec::new(
    module_name~,
    func_names~,
    local_names~,
    label_names~,
    type_names=match type_names {
      Some(names) =>
        Some(dfe_rewrite_name_map_filtered(names, type_remap, true))
      None => None
    },
    table_names~,
    memory_names~,
    global_names~,
    elem_names~,
    data_names~,
    field_names~,
    tag_names~,
  )
}

///|
fn dfe_funcs_semantically_equal(left : @lib.Func, right : @lib.Func) -> Bool {
  let @lib.Func(left_locals, @lib.Expr(left_body)) = left
  let @lib.Func(right_locals, @lib.Expr(right_body)) = right
  left_locals == right_locals &&
  pass_const_instruction_vectors_bits_equal(left_body, right_body)
}

///|
fn dfe_perf_start_timer_opt(perf : HotPerfSession?, name : String) -> UInt64 {
  match perf {
    Some(session) =>
      perf_start_timer(session, "detail:duplicate-function-elimination:\{name}")
    None => 0UL
  }
}

///|
fn dfe_perf_stop_timer_opt(
  perf : HotPerfSession?,
  name : String,
  start_us : UInt64,
) -> Unit {
  match perf {
    Some(session) =>
      perf_stop_timer(
        session,
        "detail:duplicate-function-elimination:\{name}",
        start_us,
      )
    None => ()
  }
}

///|
fn dfe_trace_opt(perf : HotPerfSession?, msg : String) -> Unit {
  match perf {
    Some(session) =>
      if session.enabled {
        (session.trace)("detail:duplicate-function-elimination:\{msg}")
      }
    None => ()
  }
}

///|
fn dfe_functions_equivalent(
  left_canon_type : UInt32,
  left_func : @lib.Func,
  left_annotations : Array[@lib.FuncAnnotation],
  right_canon_type : UInt32,
  right_func : @lib.Func,
  right_annotations : Array[@lib.FuncAnnotation],
) -> Bool {
  if !dfe_funcs_semantically_equal(left_func, right_func) ||
    left_annotations != right_annotations {
    return false
  }
  left_canon_type == right_canon_type
}

///|
fn dfe_functions_equivalent_no_annotations(
  left_canon_type : UInt32,
  left_func : @lib.Func,
  right_canon_type : UInt32,
  right_func : @lib.Func,
) -> Bool {
  if !dfe_funcs_semantically_equal(left_func, right_func) {
    return false
  }
  left_canon_type == right_canon_type
}

///|
fn dfe_shape_instruction_array_hash(
  instrs : Array[@lib.Instruction],
  mentions_type_idxs~ : Array[Bool]? = None,
  needs_debris_cleanup~ : Array[Bool]? = None,
  max_func_target~ : Array[Int32]? = None,
) -> Int32 {
  let mut key = instrs.length().hash()
  for idx = 0; idx < instrs.length(); idx = idx + 1 {
    match needs_debris_cleanup {
      Some(flag) =>
        if !flag[0] {
          match instrs[idx] {
            @lib.Unreachable | @lib.Block(_, _) => {
              let mut next = idx + 1
              while next < instrs.length() {
                match instrs[next] {
                  @lib.Drop => next += 1
                  _ => break
                }
              }
              if next < instrs.length() && instrs[next] is @lib.Unreachable {
                match instrs[idx] {
                  @lib.Unreachable => flag[0] = true
                  @lib.Block(_, _) =>
                    if pass_raw_dropped_block_has_unreachable_tail(instrs[idx]) ||
                      pass_raw_void_block_can_inline_before_unreachable(
                        instrs[idx],
                      ) {
                      flag[0] = true
                    }
                  _ => ()
                }
              }
            }
            _ => ()
          }
        }
      None => ()
    }
    key = key * 31 +
      dfe_shape_instruction_hash(
        instrs[idx],
        mentions_type_idxs~,
        needs_debris_cleanup~,
        max_func_target~,
      )
  }
  key
}

///|
fn dfe_shape_instruction_hash(
  instr : @lib.Instruction,
  mentions_type_idxs~ : Array[Bool]? = None,
  needs_debris_cleanup~ : Array[Bool]? = None,
  max_func_target~ : Array[Int32]? = None,
) -> Int32 {
  match instr {
    @lib.Block(block_type, @lib.Expr(body)) => {
      if dfe_block_type_mentions_type_idxs(block_type) {
        match mentions_type_idxs {
          Some(flag) => flag[0] = true
          None => ()
        }
      }
      1009 * 31 +
      dfe_shape_instruction_array_hash(
        body,
        mentions_type_idxs~,
        needs_debris_cleanup~,
        max_func_target~,
      )
    }
    @lib.Loop(block_type, @lib.Expr(body)) => {
      if dfe_block_type_mentions_type_idxs(block_type) {
        match mentions_type_idxs {
          Some(flag) => flag[0] = true
          None => ()
        }
      }
      1013 * 31 +
      dfe_shape_instruction_array_hash(
        body,
        mentions_type_idxs~,
        needs_debris_cleanup~,
        max_func_target~,
      )
    }
    @lib.If(block_type, then_body, else_body) => {
      if dfe_block_type_mentions_type_idxs(block_type) {
        match mentions_type_idxs {
          Some(flag) => flag[0] = true
          None => ()
        }
      }
      let mut key = 1019 * 31 +
        dfe_shape_instruction_array_hash(
          then_body,
          mentions_type_idxs~,
          needs_debris_cleanup~,
          max_func_target~,
        )
      match else_body {
        Some(body) =>
          key = key * 31 +
            dfe_shape_instruction_array_hash(
              body,
              mentions_type_idxs~,
              needs_debris_cleanup~,
              max_func_target~,
            )
        None => key *= 31
      }
      key
    }
    @lib.TryTable(block_type, catches, @lib.Expr(body)) => {
      if dfe_block_type_mentions_type_idxs(block_type) {
        match mentions_type_idxs {
          Some(flag) => flag[0] = true
          None => ()
        }
      }
      (1021 * 31 + catches.hash()) * 31 +
      dfe_shape_instruction_array_hash(
        body,
        mentions_type_idxs~,
        needs_debris_cleanup~,
        max_func_target~,
      )
    }
    @lib.Try(block_type, @lib.Expr(body), catches, delegate) => {
      if dfe_block_type_mentions_type_idxs(block_type) {
        match mentions_type_idxs {
          Some(flag) => flag[0] = true
          None => ()
        }
      }
      let mut key = (1023 * 31 + delegate.hash()) * 31 +
        dfe_shape_instruction_array_hash(
          body,
          mentions_type_idxs~,
          needs_debris_cleanup~,
          max_func_target~,
        )
      for catch_ in catches {
        let expr = match catch_ {
          @lib.LegacyCatch(tag, expr) => {
            key = (key * 31 + 1025) * 31 + tag.hash()
            expr
          }
          @lib.LegacyCatchAll(expr) => {
            key = key * 31 + 1027
            expr
          }
        }
        let @lib.Expr(catch_body) = expr
        key = key * 31 +
          dfe_shape_instruction_array_hash(
            catch_body,
            mentions_type_idxs~,
            needs_debris_cleanup~,
            max_func_target~,
          )
      }
      key
    }
    @lib.Call(func_idx) => {
      match max_func_target {
        Some(max_target) => {
          let @lib.FuncIdx(raw) = func_idx
          let target = raw.reinterpret_as_int()
          if target > max_target[0] {
            max_target[0] = target
          }
        }
        None => ()
      }
      1031
    }
    @lib.ReturnCall(func_idx) => {
      match max_func_target {
        Some(max_target) => {
          let @lib.FuncIdx(raw) = func_idx
          let target = raw.reinterpret_as_int()
          if target > max_target[0] {
            max_target[0] = target
          }
        }
        None => ()
      }
      1033
    }
    @lib.RefFunc(func_idx) => {
      match max_func_target {
        Some(max_target) => {
          let @lib.FuncIdx(raw) = func_idx
          let target = raw.reinterpret_as_int()
          if target > max_target[0] {
            max_target[0] = target
          }
        }
        None => ()
      }
      1039
    }
    _ =>
      if dfe_instruction_mentions_type_idxs(instr) {
        match mentions_type_idxs {
          Some(flag) => flag[0] = true
          None => ()
        }
        1049
      } else {
        instr.hash()
      }
  }
}

///|
fn dfe_shape_locals_hash(locals : @lib.Locals) -> Int32 {
  let mut key = 17
  for run in locals.runs() {
    key = key * 31 + run.count().hash()
    let val_type_hash = if dfe_val_type_mentions_type_idxs(run.val_type()) {
      1
    } else {
      run.val_type().hash()
    }
    key = key * 31 + val_type_hash
  }
  key
}

///|
fn dfe_function_shape_hash_no_annotations(
  type_hash : Int32,
  func : @lib.Func,
  mentions_type_idxs~ : Array[Bool]? = None,
  needs_debris_cleanup~ : Array[Bool]? = None,
  max_func_target~ : Array[Int32]? = None,
) -> Int32 {
  let @lib.Func(locals, @lib.Expr(instrs)) = func
  for run in locals.runs() {
    if dfe_val_type_mentions_type_idxs(run.val_type()) {
      match mentions_type_idxs {
        Some(flag) => flag[0] = true
        None => ()
      }
    }
  }
  (
    (dfe_shape_locals_hash(locals) * 31 + type_hash) * 31 +
    instrs.length().hash()
  ) *
  31 +
  dfe_shape_instruction_array_hash(
    instrs,
    mentions_type_idxs~,
    needs_debris_cleanup~,
    max_func_target~,
  )
}

///|
fn dfe_function_shape_hash(
  type_hash : Int32,
  func : @lib.Func,
  annotations : Array[@lib.FuncAnnotation],
  mentions_type_idxs~ : Array[Bool]? = None,
  needs_debris_cleanup~ : Array[Bool]? = None,
  max_func_target~ : Array[Int32]? = None,
) -> Int32 {
  dfe_function_shape_hash_no_annotations(
    type_hash,
    func,
    mentions_type_idxs~,
    needs_debris_cleanup~,
    max_func_target~,
  ) *
  31 +
  annotations.hash()
}

///|
fn dfe_canonical_local_idx(
  replacements : Array[Int32],
  local_idx : Int32,
) -> Int32 {
  let mut current = local_idx
  while true {
    let next = replacements[current]
    if next >= 0 {
      current = next
    } else {
      return current
    }
  }
  current
}

///|
fn dfe_type_idx_array_uses_remap(
  type_idxs : Array[@lib.TypeIdx],
  remap : Map[UInt32, UInt32],
) -> Bool {
  for type_idx in type_idxs {
    if dfe_type_idx_is_remapped(type_idx, remap) {
      return true
    }
  }
  false
}

///|
fn dfe_module_type_refs_use_remap(
  mod_ : @lib.Module,
  remap : Map[UInt32, UInt32],
) -> Bool {
  match mod_.type_sec {
    Some(@lib.TypeSec(rec_types)) =>
      for rec_type in rec_types {
        if dfe_rec_type_uses_remap(rec_type, remap) {
          return true
        }
      }
    None => ()
  }
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) =>
      for import_ in imports {
        if dfe_rewrite_import_type_idxs(import_, remap) != import_ {
          return true
        }
      }
    None => ()
  }
  match mod_.func_sec {
    Some(@lib.FuncSec(type_idxs)) =>
      if dfe_type_idx_array_uses_remap(type_idxs, remap) {
        return true
      }
    None => ()
  }
  match mod_.table_sec {
    Some(@lib.TableSec(tables)) =>
      for table in tables {
        if dfe_rewrite_table_type_idxs(table, remap) != table {
          return true
        }
      }
    None => ()
  }
  match mod_.tag_sec {
    Some(@lib.TagSec(tags)) =>
      for tag in tags {
        let @lib.TagType(type_idx) = tag
        if dfe_type_idx_is_remapped(type_idx, remap) {
          return true
        }
      }
    None => ()
  }
  match mod_.global_sec {
    Some(@lib.GlobalSec(globals)) =>
      for global in globals {
        if dfe_rewrite_global_type_idxs(global, remap) != global {
          return true
        }
      }
    None => ()
  }
  match mod_.elem_sec {
    Some(@lib.ElemSec(elems)) =>
      for elem in elems {
        if dfe_rewrite_elem_type_idxs(elem, remap) != elem {
          return true
        }
      }
    None => ()
  }
  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) =>
      for func in funcs {
        if dfe_scan_rewrite_func_type_idxs(func, remap).rewritten is Some(_) {
          return true
        }
      }
    None => ()
  }
  match mod_.data_sec {
    Some(@lib.DataSec(datas)) =>
      for data in datas {
        if dfe_rewrite_data_type_idxs(data, remap) != data {
          return true
        }
      }
    None => ()
  }
  false
}

///|
fn dfe_rec_type_is_unused_suffix_prune_candidate(
  rec_type : @lib.RecType,
) -> Bool {
  match rec_type {
    @lib.SingleRecType(subtype) => {
      if subtype.super_types().length() > 0 ||
        subtype.describes_type() is Some(_) ||
        subtype.descriptor_type() is Some(_) {
        return false
      }
      match subtype.get_comptype() {
        @lib.FuncCompType(params, results) =>
          !dfe_val_types_mention_type_idxs(params) &&
          !dfe_val_types_mention_type_idxs(results)
        _ => false
      }
    }
    @lib.GroupRecType(_) => false
  }
}

///|
fn dfe_prune_unused_simple_types(
  mod_ : @lib.Module,
  root_scans~ : Array[Int32]? = None,
) -> @lib.Module {
  match mod_.type_sec {
    Some(@lib.TypeSec(rec_types)) => {
      if rec_types.length() == 0 {
        return mod_
      }
      for rec_type in rec_types {
        match rec_type {
          @lib.SingleRecType(_) => ()
          @lib.GroupRecType(_) => return mod_
        }
      }
      let live : Array[Bool] = Array::make(rec_types.length(), false)
      // Function-section references are direct and common. Avoid probing the
      // entire module separately for each already-live signature.
      match mod_.func_sec {
        Some(@lib.FuncSec(type_idxs)) =>
          for entry in type_idxs {
            match entry {
              @lib.TypeIdx(raw_idx) => {
                let type_idx = raw_idx.reinterpret_as_int()
                if type_idx >= 0 && type_idx < live.length() {
                  live[type_idx] = true
                }
              }
              @lib.RecIdx(_) => ()
            }
          }
        None => ()
      }
      match mod_.import_sec {
        Some(@lib.ImportSec(imports)) =>
          for import_ in imports {
            match import_ {
              @lib.Import(_, _, @lib.FuncExternType(@lib.TypeIdx(raw_idx))) => {
                let type_idx = raw_idx.reinterpret_as_int()
                if type_idx >= 0 && type_idx < live.length() {
                  live[type_idx] = true
                }
              }
              @lib.Import(
                _,
                _,
                @lib.TagExternType(@lib.TagType(@lib.TypeIdx(raw_idx)))
              ) => {
                let type_idx = raw_idx.reinterpret_as_int()
                if type_idx >= 0 && type_idx < live.length() {
                  live[type_idx] = true
                }
              }
              _ => ()
            }
          }
        None => ()
      }
      match mod_.tag_sec {
        Some(@lib.TagSec(tags)) =>
          for tag in tags {
            match tag {
              @lib.TagType(@lib.TypeIdx(raw_idx)) => {
                let type_idx = raw_idx.reinterpret_as_int()
                if type_idx >= 0 && type_idx < live.length() {
                  live[type_idx] = true
                }
              }
              _ => ()
            }
          }
        None => ()
      }
      if live.all(fn(is_live) { is_live }) {
        return mod_
      }
      let refs : DfeTypeRefs = { live, pending: [], }
      for idx in 0.. count[0] += 1
        None => ()
      }
      dfe_type_refs_module_roots(mod_, refs)
      let mut cursor = 0
      while cursor < refs.pending.length() {
        dfe_type_refs_rec_type(rec_types[refs.pending[cursor]], refs)
        cursor += 1
      }
      let mut idx = 0
      let kept : Array[@lib.RecType] = []
      let remap : Map[UInt32, UInt32] = Map([])
      idx = 0
      while idx < rec_types.length() {
        if live[idx] {
          remap[idx.reinterpret_as_uint()] = kept.length().reinterpret_as_uint()
          kept.push(rec_types[idx])
        }
        idx += 1
      }
      if kept.length() == rec_types.length() {
        return mod_
      }
      let rewritten = dfe_rewrite_module_type_idxs(
        mod_.with_type_sec(@lib.TypeSec::new(kept)),
        remap,
      )
      match rewritten.name_sec {
        Some(name_sec) =>
          rewritten.with_name_sec(dfe_rewrite_type_name_sec(name_sec, remap))
        None => rewritten
      }
    }
    None => mod_
  }
}

///|
fn dfe_prune_unused_simple_func_type_suffix(mod_ : @lib.Module) -> @lib.Module {
  match mod_.type_sec {
    Some(@lib.TypeSec(rec_types)) => {
      let mut keep_count = rec_types.length()
      while keep_count > 0 {
        let candidate_idx = keep_count - 1
        if !dfe_rec_type_is_unused_suffix_prune_candidate(
            rec_types[candidate_idx],
          ) {
          break
        }
        let remap : Map[UInt32, UInt32] = Map([])
        remap[candidate_idx.reinterpret_as_uint()] = 0U
        if dfe_module_type_refs_use_remap(mod_, remap) {
          break
        }
        keep_count -= 1
      }
      if keep_count == rec_types.length() {
        return mod_
      }
      let kept : Array[@lib.RecType] = []
      let keep_remap : Map[UInt32, UInt32] = Map([])
      for idx = 0; idx < keep_count; idx = idx + 1 {
        kept.push(rec_types[idx])
        keep_remap[idx.reinterpret_as_uint()] = idx.reinterpret_as_uint()
      }
      let with_types = mod_.with_type_sec(@lib.TypeSec::new(kept))
      match with_types.name_sec {
        Some(name_sec) =>
          with_types.with_name_sec(
            dfe_rewrite_type_name_sec(name_sec, keep_remap),
          )
        None => with_types
      }
    }
    None => mod_
  }
}

///|
fn dfe_canonicalize_duplicate_simple_type_indices(
  mod_ : @lib.Module,
  perf~ : HotPerfSession? = None,
  code_rewrite_flags~ : @bitset.BitSet? = None,
) -> @lib.Module {
  match mod_.type_sec {
    Some(@lib.TypeSec(rec_types)) => {
      let map_timer = dfe_perf_start_timer_opt(perf, "type-canon:map")
      let canonical_old_idxs = dfe_duplicate_simple_type_canonical_map(
        rec_types,
      )
      dfe_perf_stop_timer_opt(perf, "type-canon:map", map_timer)
      if canonical_old_idxs.length() == 0 {
        return mod_
      }
      let keep_timer = dfe_perf_start_timer_opt(perf, "type-canon:kept-types")
      let kept_types : Array[@lib.RecType] = []
      let old_to_new : Map[UInt32, UInt32] = Map([])
      let mut old_idx = 0
      let mut next_idx = 0
      for rec_type in rec_types {
        let member_count = dfe_rec_type_member_count(rec_type)
        let raw_old_idx = old_idx.reinterpret_as_uint()
        if canonical_old_idxs.get(raw_old_idx) is None {
          for member = 0; member < member_count; member = member + 1 {
            old_to_new[(old_idx + member).reinterpret_as_uint()] = (next_idx +
            member).reinterpret_as_uint()
          }
          kept_types.push(rec_type)
          next_idx += member_count
        }
        old_idx += member_count
      }
      let flat_type_count = old_idx
      dfe_perf_stop_timer_opt(perf, "type-canon:kept-types", keep_timer)
      if kept_types.length() == rec_types.length() {
        return mod_
      }
      let remap_timer = dfe_perf_start_timer_opt(perf, "type-canon:remap")
      // Operand rewriting needs only changed indices; explicit identity entries
      // make the shared optional reference rewriter reconstruct unchanged trees.
      // Names still need dense ownership/remapping to preserve unmoved entries.
      let type_remap : Map[UInt32, UInt32] = Map([])
      let name_remap : Map[UInt32, UInt32]? = match mod_.name_sec {
        Some(_) => Some(Map([]))
        None => None
      }
      old_idx = 0
      while old_idx < flat_type_count {
        let raw_old_idx = old_idx.reinterpret_as_uint()
        let canonical_old_idx = canonical_old_idxs
          .get(raw_old_idx)
          .unwrap_or(raw_old_idx)
        match old_to_new.get(canonical_old_idx) {
          Some(new_idx) => {
            if raw_old_idx != new_idx {
              type_remap[raw_old_idx] = new_idx
            }
            match name_remap {
              Some(names) => names[raw_old_idx] = new_idx
              None => ()
            }
          }
          None => return mod_
        }
        old_idx += 1
      }
      dfe_perf_stop_timer_opt(perf, "type-canon:remap", remap_timer)
      let rewrite_timer = dfe_perf_start_timer_opt(perf, "type-canon:rewrite")
      let rewritten = dfe_rewrite_module_type_idxs(
        mod_,
        type_remap,
        perf~,
        code_rewrite_flags~,
        rewrite_type_sec=false,
      )
      let rewritten_kept_types : Array[@lib.RecType] = []
      for kept_type in kept_types {
        if dfe_rec_type_uses_remap(kept_type, type_remap) {
          rewritten_kept_types.push(dfe_rewrite_rec_type(kept_type, type_remap))
        } else {
          rewritten_kept_types.push(kept_type)
        }
      }
      dfe_perf_stop_timer_opt(perf, "type-canon:rewrite", rewrite_timer)
      let with_types = rewritten.with_type_sec(
        @lib.TypeSec::new(rewritten_kept_types),
      )
      match (with_types.name_sec, name_remap) {
        (Some(name_sec), Some(names)) =>
          with_types.with_name_sec(dfe_rewrite_type_name_sec(name_sec, names))
        _ => with_types
      }
    }
    None => mod_
  }
}

///|
priv struct DfeEliminationResult {
  mod_ : @lib.Module
  changed : Bool
  iteration_count : Int32
  type_rewrite_flags : @bitset.BitSet?
  cleanup_flags : @bitset.BitSet?
}

///|
fn dfe_canonical_abs_remap(
  replacements : Array[Int32],
  imported_count : Int32,
) -> Array[UInt32] {
  let remap : Array[UInt32] = Array::new(
    capacity=imported_count + replacements.length(),
  )
  for idx = 0; idx < imported_count; idx = idx + 1 {
    remap.push(idx.reinterpret_as_uint())
  }
  for local_idx = 0
      local_idx < replacements.length()
      local_idx = local_idx + 1 {
    remap.push(
      (imported_count + dfe_canonical_local_idx(replacements, local_idx)).reinterpret_as_uint(),
    )
  }
  remap
}

///|
fn dfe_type_normalized_candidate(
  funcs : Array[@lib.Func],
  local_idx : Int32,
  type_canon : Map[UInt32, UInt32],
  cache : Array[@lib.Func?],
  ready : Array[Bool],
  normalization_visits : Array[Int32]?,
  normalized_rewrite_count : Array[Int32],
) -> @lib.Func {
  if !ready[local_idx] {
    match normalization_visits {
      Some(visits) => visits[0] += 1
      None => ()
    }
    let func = funcs[local_idx]
    let normalized = if type_canon.length() == 0 {
      func
    } else {
      let scan = dfe_scan_rewrite_func_type_idxs(func, type_canon)
      match scan.rewritten {
        Some(next) => {
          normalized_rewrite_count[0] += 1
          next
        }
        None => func
      }
    }
    cache[local_idx] = Some(normalized)
    ready[local_idx] = true
  }
  cache[local_idx].unwrap()
}

///|
fn dfe_eliminate_duplicate_functions(
  mod_ : @lib.Module,
  perf~ : HotPerfSession? = None,
  hash_visits~ : Array[Int32]? = None,
  normalization_visits~ : Array[Int32]? = None,
) -> DfeEliminationResult {
  match (mod_.func_sec, mod_.code_sec) {
    (Some(@lib.FuncSec(type_idxs)), Some(@lib.CodeSec(funcs))) => {
      if type_idxs.length() != funcs.length() {
        return {
          mod_,
          changed: false,
          iteration_count: 0,
          type_rewrite_flags: None,
          cleanup_flags: None,
        }
      }
      let type_canon_timer = dfe_perf_start_timer_opt(perf, "type-canon")
      let type_canon = match mod_.type_sec {
        Some(@lib.TypeSec(rec_types)) =>
          dfe_duplicate_simple_type_canonical_map(rec_types)
        None => Map([])
      }
      dfe_perf_stop_timer_opt(perf, "type-canon", type_canon_timer)
      let imported_count = dfe_imported_func_count(mod_)
      let identity_visible = dfe_identity_visible_defined_funcs(
        mod_,
        imported_count,
        funcs.length(),
      )
      let annotations_by_abs_idx = dfe_func_annotation_map(mod_)
      let has_annotations = annotations_by_abs_idx.length() > 0
      let normalized_funcs : Array[@lib.Func?] = Array::make(
        funcs.length(),
        None,
      )
      let normalized_ready = Array::make(funcs.length(), false)
      let normalized_rewrite_count = [0]

      let canonical_type_raws : Array[UInt32] = Array::new(
        capacity=type_idxs.length(),
      )
      let canonical_type_hashes : Array[Int32] = Array::new(
        capacity=type_idxs.length(),
      )
      for type_idx in type_idxs {
        let canonical_raw = match type_idx {
          @lib.TypeIdx(raw) => type_canon.get(raw).unwrap_or(raw)
          @lib.RecIdx(raw) => raw
        }
        canonical_type_raws.push(canonical_raw)
        canonical_type_hashes.push(canonical_raw.hash())
      }
      let normalized_annotations : Array[Array[@lib.FuncAnnotation]] = Array::new(
        capacity=funcs.length(),
      )
      if has_annotations {
        for local_idx = 0; local_idx < funcs.length(); local_idx = local_idx + 1 {
          normalized_annotations.push(
            dfe_func_annotations(
              annotations_by_abs_idx,
              (imported_count + local_idx).reinterpret_as_uint(),
            ),
          )
        }
      }

      let hash_group_timer = dfe_perf_start_timer_opt(perf, "hash-groups")
      let first_by_hash : @hashmap.HashMap[Int32, Int32] = @hashmap.new(
        capacity=funcs.length(),
      )
      let collision_group_by_hash : @hashmap.HashMap[Int32, Int32] = @hashmap.new(
        capacity=funcs.length() / 8 + 1,
      )
      let hash_groups : Array[Array[Int32]] = []
      let original_type_rewrite_flags = @bitset.BitSet::new(funcs.length())
      let original_cleanup_flags = @bitset.BitSet::new(funcs.length())
      let max_func_target_by_local : Array[Int32] = []
      for local_idx = 0; local_idx < funcs.length(); local_idx = local_idx + 1 {
        match hash_visits {
          Some(visits) => visits[0] += 1
          None => ()
        }
        let mentions_type_idxs = [false]
        let needs_debris_cleanup = [false]
        let max_func_target = [-1]
        let key = if has_annotations {
          dfe_function_shape_hash(
            canonical_type_hashes[local_idx],
            funcs[local_idx],
            normalized_annotations[local_idx],
            mentions_type_idxs=Some(mentions_type_idxs),
            needs_debris_cleanup=Some(needs_debris_cleanup),
            max_func_target=Some(max_func_target),
          )
        } else {
          dfe_function_shape_hash_no_annotations(
            canonical_type_hashes[local_idx],
            funcs[local_idx],
            mentions_type_idxs=Some(mentions_type_idxs),
            needs_debris_cleanup=Some(needs_debris_cleanup),
            max_func_target=Some(max_func_target),
          )
        }
        max_func_target_by_local.push(max_func_target[0])
        if mentions_type_idxs[0] {
          original_type_rewrite_flags[local_idx] = true
        }
        if needs_debris_cleanup[0] {
          original_cleanup_flags[local_idx] = true
        }
        // Protected identities can be neither merged nor representatives.
        // Keep their rewrite/cleanup facts above, but exclude them before
        // normalization and collision grouping to avoid protected-only pair
        // enumeration when presets admit modules with host boundaries.
        if identity_visible[local_idx] {
          continue
        }
        match first_by_hash.get(key) {
          Some(first_idx) =>
            match collision_group_by_hash.get(key) {
              Some(group_idx) => hash_groups[group_idx].push(local_idx)
              None => {
                collision_group_by_hash.set(key, hash_groups.length())
                hash_groups.push([first_idx, local_idx])
              }
            }
          None => first_by_hash.set(key, local_idx)
        }
      }
      dfe_perf_stop_timer_opt(perf, "hash-groups", hash_group_timer)
      dfe_trace_opt(
        perf,
        "hash groups=\{hash_groups.length()} singleton_hashes=\{first_by_hash.length()}",
      )

      let replacements : Array[Int32] = Array::make(funcs.length(), -1)
      let collision_timer = dfe_perf_start_timer_opt(perf, "collision-check")
      let mut changed = false
      let mut iteration_count = 0
      let mut comparison_count = 0
      let mut replacement_count = 0
      while true {
        let mut first_replaced_abs = -1
        for local_idx = 0
            local_idx < replacements.length()
            local_idx = local_idx + 1 {
          if replacements[local_idx] >= 0 {
            first_replaced_abs = imported_count + local_idx
            break
          }
        }
        let canonical_abs = dfe_canonical_abs_remap(
          replacements, imported_count,
        )
        let mut round_changed = false
        for group in hash_groups {
          if first_replaced_abs >= 0 {
            let mut may_change = false
            for local_idx in group {
              if replacements[local_idx] < 0 &&
                max_func_target_by_local[local_idx] >= first_replaced_abs {
                may_change = true
                break
              }
            }
            // Unchanged function bodies were already compared in the prior
            // round; they cannot form a new duplicate until a call target moves.
            if !may_change {
              continue
            }
          }
          let live_indices : Array[Int32] = []
          let live_funcs : Array[@lib.Func] = []
          for local_idx in group {
            if replacements[local_idx] < 0 {
              live_indices.push(local_idx)
              let normalized = dfe_type_normalized_candidate(
                funcs, local_idx, type_canon, normalized_funcs, normalized_ready,
                normalization_visits, normalized_rewrite_count,
              )
              live_funcs.push(
                if first_replaced_abs < 0 ||
                  max_func_target_by_local[local_idx] < first_replaced_abs {
                  // Before the first replacement the map is the identity.
                  // Later epochs cannot change a target below its first delta.
                  normalized
                } else {
                  dfe_rewrite_func_func_idxs(normalized, canonical_abs)
                },
              )
            }
          }
          let comparison_groups : Array[Array[Int32]] = []
          // A run of equal, mergeable members is cheaper to compare directly.
          // The exact buckets help when one shape contains many distinct bodies.
          let mut likely_duplicate_group = live_indices.length() >= 8
          if likely_duplicate_group {
            let first = live_indices[0]
            if identity_visible[first] {
              likely_duplicate_group = false
            } else {
              for sample_idx = 1
                  sample_idx < live_indices.length()
                  sample_idx = sample_idx + 1 {
                let next = live_indices[sample_idx]
                if identity_visible[next] ||
                  canonical_type_raws[first] != canonical_type_raws[next] ||
                  live_funcs[0] != live_funcs[sample_idx] ||
                  (
                    has_annotations &&
                    normalized_annotations[first] !=
                    normalized_annotations[next]
                  ) {
                  likely_duplicate_group = false
                  break
                }
              }
            }
          }
          if likely_duplicate_group {
            let all_live : Array[Int32] = []
            for live_idx = 0
                live_idx < live_indices.length()
                live_idx = live_idx + 1 {
              all_live.push(live_idx)
            }
            comparison_groups.push(all_live)
          } else {
            let first_by_exact_hash : @hashmap.HashMap[Int32, Int32] = @hashmap.new(
              capacity=live_indices.length(),
            )
            let collision_group_by_exact_hash : @hashmap.HashMap[Int32, Int32] = @hashmap.new()
            for live_idx = 0
                live_idx < live_indices.length()
                live_idx = live_idx + 1 {
              let local_idx = live_indices[live_idx]
              let mut key = canonical_type_raws[local_idx].hash() * 31 +
                live_funcs[live_idx].hash()
              if has_annotations {
                key = key * 31 + normalized_annotations[local_idx].hash()
              }
              match first_by_exact_hash.get(key) {
                Some(first_idx) =>
                  match collision_group_by_exact_hash.get(key) {
                    Some(group_idx) =>
                      comparison_groups[group_idx].push(live_idx)
                    None => {
                      collision_group_by_exact_hash.set(
                        key,
                        comparison_groups.length(),
                      )
                      comparison_groups.push([first_idx, live_idx])
                    }
                  }
                None => first_by_exact_hash.set(key, live_idx)
              }
            }
          }
          for comparison_group in comparison_groups {
            let mut i = 0
            while i + 1 < comparison_group.length() {
              let first_live_idx = comparison_group[i]
              let first = live_indices[first_live_idx]
              if replacements[first] >= 0 {
                i += 1
                continue
              }
              let mut j = i + 1
              while j < comparison_group.length() {
                let second_live_idx = comparison_group[j]
                let second = live_indices[second_live_idx]
                if replacements[second] >= 0 {
                  j += 1
                  continue
                }
                // Exported and address-materialized functions have observable
                // identities, including through host-visible tables and globals.
                if identity_visible[first] || identity_visible[second] {
                  j += 1
                  continue
                }
                comparison_count += 1
                let equivalent = if has_annotations {
                  dfe_functions_equivalent(
                    canonical_type_raws[first],
                    live_funcs[first_live_idx],
                    normalized_annotations[first],
                    canonical_type_raws[second],
                    live_funcs[second_live_idx],
                    normalized_annotations[second],
                  )
                } else {
                  dfe_functions_equivalent_no_annotations(
                    canonical_type_raws[first],
                    live_funcs[first_live_idx],
                    canonical_type_raws[second],
                    live_funcs[second_live_idx],
                  )
                }
                if equivalent {
                  replacements[second] = first
                  changed = true
                  round_changed = true
                  replacement_count += 1
                }
                j += 1
              }
              i += 1
            }
          }
        }
        if !round_changed {
          break
        }
        iteration_count += 1
      }
      dfe_perf_stop_timer_opt(perf, "collision-check", collision_timer)
      dfe_trace_opt(
        perf,
        "normalize rewritten_funcs=\{normalized_rewrite_count[0]} total_funcs=\{funcs.length()}",
      )
      dfe_trace_opt(
        perf,
        "collision comparisons=\{comparison_count} replacements=\{replacement_count}",
      )
      if !changed {
        return {
          mod_,
          changed: false,
          iteration_count,
          type_rewrite_flags: None,
          cleanup_flags: None,
        }
      }

      let rebuild_timer = dfe_perf_start_timer_opt(perf, "rebuild")
      let kept_types : Array[@lib.TypeIdx] = Array::new(capacity=funcs.length())
      let kept_funcs : Array[@lib.Func] = Array::new(capacity=funcs.length())
      let kept_abs : Array[Int32] = Array::make(funcs.length(), -1)
      let kept_original_indices : Array[Int32] = []
      let kept_type_rewrite_flags = @bitset.BitSet::new(
        funcs.length() - replacement_count,
      )
      let kept_cleanup_flags = @bitset.BitSet::new(
        funcs.length() - replacement_count,
      )
      let kept_timer = dfe_perf_start_timer_opt(perf, "rebuild:kept")
      let mut next_abs = imported_count
      for idx = 0; idx < funcs.length(); idx = idx + 1 {
        if replacements[idx] < 0 {
          kept_abs[idx] = next_abs
          kept_original_indices.push(idx)
          kept_types.push(type_idxs[idx])
          kept_funcs.push(funcs[idx])
          if original_type_rewrite_flags[idx] {
            kept_type_rewrite_flags[kept_funcs.length() - 1] = true
          }
          if original_cleanup_flags[idx] {
            kept_cleanup_flags[kept_funcs.length() - 1] = true
          }
          next_abs += 1
        }
      }
      dfe_perf_stop_timer_opt(perf, "rebuild:kept", kept_timer)
      dfe_trace_opt(
        perf,
        "rebuild kept_funcs=\{kept_funcs.length()} removed_funcs=\{funcs.length() - kept_funcs.length()}",
      )
      let remap_timer = dfe_perf_start_timer_opt(perf, "rebuild:remap")
      let abs_remap : Array[UInt32] = Array::new(
        capacity=imported_count + funcs.length(),
      )
      for idx = 0; idx < imported_count; idx = idx + 1 {
        abs_remap.push(idx.reinterpret_as_uint())
      }
      for idx = 0; idx < funcs.length(); idx = idx + 1 {
        let canonical = dfe_canonical_local_idx(replacements, idx)
        abs_remap.push(kept_abs[canonical].reinterpret_as_uint())
      }
      let mut first_removed_abs = -1
      for local_idx = 0
          local_idx < replacements.length()
          local_idx = local_idx + 1 {
        if replacements[local_idx] >= 0 {
          first_removed_abs = imported_count + local_idx
          break
        }
      }
      let kept_func_rewrite_flags = @bitset.BitSet::new(kept_funcs.length())
      let mut func_rewrite_count = 0
      for kept_idx = 0
          kept_idx < kept_original_indices.length()
          kept_idx = kept_idx + 1 {
        let original_idx = kept_original_indices[kept_idx]
        if first_removed_abs >= 0 &&
          max_func_target_by_local[original_idx] >= first_removed_abs {
          kept_func_rewrite_flags[kept_idx] = true
          func_rewrite_count += 1
        }
      }
      dfe_perf_stop_timer_opt(perf, "rebuild:remap", remap_timer)
      dfe_trace_opt(perf, "rebuild func_rewrite_count=\{func_rewrite_count}")
      let compacted = mod_
        .without_name_sec()
        .with_func_sec(@lib.FuncSec::new(kept_types))
        .with_code_sec(@lib.CodeSec::new(kept_funcs))
      let rewrite_timer = dfe_perf_start_timer_opt(
        perf, "rebuild:rewrite-module",
      )
      let rewritten = dfe_rewrite_module_func_idxs(
        compacted,
        abs_remap,
        perf~,
        code_rewrite_flags=Some(kept_func_rewrite_flags),
      )
      dfe_perf_stop_timer_opt(perf, "rebuild:rewrite-module", rewrite_timer)
      dfe_perf_stop_timer_opt(perf, "rebuild", rebuild_timer)
      {
        mod_: rewritten,
        changed: true,
        iteration_count,
        type_rewrite_flags: Some(kept_type_rewrite_flags),
        cleanup_flags: Some(kept_cleanup_flags),
      }
    }
    _ =>
      {
        mod_,
        changed: false,
        iteration_count: 0,
        type_rewrite_flags: None,
        cleanup_flags: None,
      }
  }
}

///|
fn dfe_instrs_need_unreachable_debris_cleanup(
  instrs : Array[@lib.Instruction],
) -> Bool {
  let mut idx = 0
  while idx < instrs.length() {
    match instrs[idx] {
      @lib.Unreachable | @lib.Block(_, _) => {
        let mut next = idx + 1
        while next < instrs.length() {
          match instrs[next] {
            @lib.Drop => next += 1
            _ => break
          }
        }
        if next < instrs.length() && instrs[next] is @lib.Unreachable {
          match instrs[idx] {
            @lib.Unreachable => return true
            @lib.Block(_, _) =>
              if pass_raw_dropped_block_has_unreachable_tail(instrs[idx]) ||
                pass_raw_void_block_can_inline_before_unreachable(instrs[idx]) {
                return true
              }
            _ => ()
          }
        }
      }
      _ => ()
    }
    let nested = match instrs[idx] {
      @lib.Block(_, @lib.Expr(body))
      | @lib.Loop(_, @lib.Expr(body))
      | @lib.TryTable(_, _, @lib.Expr(body)) =>
        dfe_instrs_need_unreachable_debris_cleanup(body)
      @lib.If(_, then_body, else_body) =>
        if dfe_instrs_need_unreachable_debris_cleanup(then_body) {
          true
        } else {
          match else_body {
            Some(body) => dfe_instrs_need_unreachable_debris_cleanup(body)
            None => false
          }
        }
      _ => false
    }
    if nested {
      return true
    }
    idx += 1
  }
  false
}

///|
fn dfe_cleanup_unreachable_debris(
  mod_ : @lib.Module,
  perf~ : HotPerfSession? = None,
  cleanup_flags~ : @bitset.BitSet? = None,
) -> @lib.Module {
  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => {
      let next : Array[@lib.Func] = []
      let mut changed = false
      let mut changed_count = 0
      for idx = 0; idx < funcs.length(); idx = idx + 1 {
        let func = funcs[idx]
        let @lib.Func(locals, @lib.Expr(instrs)) = func
        let should_clean = match cleanup_flags {
          Some(flags) if flags.length() == funcs.length() => flags[idx]
          _ => dfe_instrs_need_unreachable_debris_cleanup(instrs)
        }
        if !should_clean {
          next.push(func)
          continue
        }
        let (next_instrs, func_changed) = pass_raw_remove_dropped_unreachable_debris(
          instrs,
        )
        if func_changed {
          changed = true
          changed_count += 1
          next.push(@lib.Func::new(locals, @lib.Expr::new(next_instrs)))
        } else {
          next.push(func)
        }
      }
      if changed {
        dfe_trace_opt(perf, "cleanup changed_funcs=\{changed_count}")
        mod_.with_code_sec(@lib.CodeSec::new(next))
      } else {
        mod_
      }
    }
    None => mod_
  }
}

///|
fn dfe_run_module_pass_with_perf(
  mod_ : @lib.Module,
  perf : HotPerfSession?,
) -> @lib.Module {
  if pass_module_has_offset_sensitive_metadata(mod_) {
    return mod_
  }
  // Mixed GC modules require type canonicalization before private body
  // hashing: otherwise equivalent aggregate/signature indices prevent merging.
  // Keep the existing independent-function path and liveness-pruner guard.
  let mod_ = match mod_.type_sec {
    Some(@lib.TypeSec(rec_types)) if !dfe_supports_simple_type_dedup(rec_types) =>
      dfe_canonicalize_duplicate_simple_type_indices(mod_, perf~)
    _ => mod_
  }
  let iteration_timer = dfe_perf_start_timer_opt(perf, "iterations")
  let elimination = dfe_eliminate_duplicate_functions(mod_, perf~)
  let mut current = elimination.mod_
  let changed = elimination.changed
  dfe_perf_stop_timer_opt(perf, "iterations", iteration_timer)
  dfe_trace_opt(perf, "iterations count=\{elimination.iteration_count}")

  let cleanup_timer = dfe_perf_start_timer_opt(
    perf, "cleanup-unreachable-debris",
  )
  current = dfe_cleanup_unreachable_debris(
    current,
    perf~,
    cleanup_flags=elimination.cleanup_flags,
  )
  dfe_perf_stop_timer_opt(perf, "cleanup-unreachable-debris", cleanup_timer)

  let elem_timer = dfe_perf_start_timer_opt(perf, "canonicalize-elem-segments")
  let with_elem_segments = dfe_canonicalize_elem_segments(current)
  let with_elem_segments = dfe_strip_name_sec(with_elem_segments)
  dfe_perf_stop_timer_opt(perf, "canonicalize-elem-segments", elem_timer)
  if !changed {
    return dfe_cleanup_raw_encoding(with_elem_segments)
  }

  let type_timer = dfe_perf_start_timer_opt(
    perf, "canonicalize-duplicate-types",
  )
  let rewritten = dfe_canonicalize_duplicate_simple_type_indices(
    with_elem_segments,
    perf~,
    code_rewrite_flags=elimination.type_rewrite_flags,
  )
  dfe_perf_stop_timer_opt(perf, "canonicalize-duplicate-types", type_timer)
  dfe_cleanup_raw_encoding(rewritten)
}

///|
pub fn dfe_run_module_pass(mod_ : @lib.Module) -> @lib.Module {
  dfe_run_module_pass_with_perf(mod_, None)
}

///|
// Local grouping keeps its legacy control barrier and preserves parameter indices.
// Keep parameter indices stable and accept only an actual whole-module saving:
// fewer declaration runs can otherwise lose to wider local-index encodings.
fn dfe_group_numeric_locals_if_smaller(
  mod_ : @lib.Module,
  function_comparisons~ : Array[Int32]? = None,
) -> @lib.Module {
  if mod_.code_sec is None || pass_module_has_legacy_try(mod_) {
    return mod_
  }
  let grouped = run_hot_pipeline_raw_group_body_locals_by_type_module(mod_)
  // The grouping helper returns its input unless a nonidentity local remap
  // changed a body and its declarations; no body equality walk is needed.
  ignore(function_comparisons)
  if physical_equal(grouped, mod_) {
    return mod_
  }
  match @binary.encoded_module_sizes(mod_, grouped) {
    Ok((old_size, new_size)) if new_size < old_size =>
      if @validate.validate_module(grouped) is Ok(_) {
        return grouped
      }
    _ => ()
  }
  mod_
}

///|
fn dfe_cleanup_raw_encoding(mod_ : @lib.Module) -> @lib.Module {
  if pass_module_has_offset_sensitive_metadata(mod_) {
    return mod_
  }
  dfe_cleanup_simple_type_encoding(dfe_group_numeric_locals_if_smaller(mod_))
}

///|
fn dfe_cleanup_simple_type_encoding(
  mod_ : @lib.Module,
  full_size_checks~ : Array[Int32]? = None,
  perf~ : HotPerfSession? = None,
) -> @lib.Module {
  match
    dfe_cleanup_simple_type_encoding_candidate(mod_, full_size_checks~, perf~) {
    Some(candidate) =>
      if @validate.validate_module(candidate) is Ok(_) {
        candidate
      } else {
        mod_
      }
    None => mod_
  }
}

///|
// Build only the guarded, strictly smaller candidate. Validation belongs to
// its caller so a transaction can validate its exact final module once.
fn dfe_cleanup_simple_type_encoding_candidate(
  mod_ : @lib.Module,
  full_size_comparisons~ : Array[Int32]? = None,
  full_size_checks~ : Array[Int32]? = None,
  body_size_proofs~ : Array[Int32]? = None,
  perf~ : HotPerfSession? = None,
) -> @lib.Module? {
  if mod_.name_sec is None && mod_.raw_name_sec_payload is Some(_) {
    return None
  }
  let grouped = mod_
  let types = match grouped.type_sec {
    Some(@lib.TypeSec(types)) => types
    None => return None
  }
  // The type walker includes legacy bodies and catches. Restrict this remapper
  // to independent unshared function signatures; GC/proposal type graphs keep
  // their definitions until their complete reference surfaces are supported.
  for rec_type in types {
    match rec_type {
      @lib.SingleRecType(subtype) =>
        if subtype.metadata().is_shared() ||
          !dfe_rec_type_is_unused_suffix_prune_candidate(rec_type) {
          return None
        }
      @lib.GroupRecType(_) => return None
    }
  }
  // Distinct functions can still use equivalent signatures. The elimination
  // fast path skips type canonicalization, so intern live plain signatures here
  // as well as pruning dead definitions, under the same complete type guard.
  let intern_start = perf_start_timer_opt(
    perf, "detail:simplify-locals:encoding-type-intern",
  )
  let interned = dfe_canonicalize_duplicate_simple_type_indices(grouped, perf~)
  perf_stop_timer_opt(
    perf, "detail:simplify-locals:encoding-type-intern", intern_start,
  )
  let prune_start = perf_start_timer_opt(
    perf, "detail:simplify-locals:encoding-type-prune",
  )
  let pruned = dfe_prune_unused_simple_types(interned)
  perf_stop_timer_opt(
    perf, "detail:simplify-locals:encoding-type-prune", prune_start,
  )
  if pruned.type_sec.unwrap().0.length() == types.length() {
    return None
  }
  // Both remappers retain types in order and map duplicate types to earlier
  // entries. Every rewritten type index is therefore no greater than its old
  // index, so its LEB width cannot grow in any other section. Type names only
  // lose entries or use smaller indices. Check the sole section that can make
  // the decision without encoding and validating the entire code section.
  let before_types = @lib.Module::new().with_type_sec(@lib.TypeSec::new(types))
  let after_types = @lib.Module::new().with_type_sec(pruned.type_sec.unwrap())
  let section_size_start = perf_start_timer_opt(
    perf, "detail:simplify-locals:encoding-type-size",
  )
  let type_sizes = @binary.encoded_module_sizes(before_types, after_types)
  perf_stop_timer_opt(
    perf, "detail:simplify-locals:encoding-type-size", section_size_start,
  )
  match type_sizes {
    Ok((before, after)) if after < before => return Some(pruned)
    _ => ()
  }
  // The type-only acceptance above already selected this same candidate
  // after an inconclusive body proof. Try it first to avoid walking every
  // instruction when the strictly smaller type section decides the result.
  // Keep the original body proof and full encoding for inconclusive cases.
  match body_size_proofs {
    Some(proofs) => proofs[0] += 1
    None => ()
  }
  if @binary.encoded_type_compaction_is_strictly_smaller(grouped, pruned) {
    return Some(pruned)
  }
  if full_size_comparisons is Some(count) {
    count[0] += 1
  }
  // Keep the original guarded path if section-only encoding cannot decide.
  match full_size_checks {
    Some(checks) => checks[0] += 1
    None => ()
  }
  match @binary.encoded_module_sizes(grouped, pruned) {
    Ok((before, after)) if after < before => return Some(pruned)
    _ => ()
  }
  None
}

///|
test "type encoding cleanup preserves live signatures and bytes without a whole body size proof" {
  let input = pass_test_module_from_wat(
    (
      #|(module
      #| (type $first (func (param i32) (result i32)))
      #| (type $duplicate (func (param i32) (result i32)))
      #| (func $first (type $first) (param i32) (result i32) local.get 0)
      #| (func (export "run") (type $duplicate) (param i32) (result i32)
      #|   local.get 0 call $first))
    ),
  )
  @validate.validate_module(input).unwrap()
  let saved = @binary.encode_module(input).unwrap()
  let expected = dfe_prune_unused_simple_types(
    dfe_canonicalize_duplicate_simple_type_indices(input),
  )
  let proofs = [0]
  let output = dfe_cleanup_simple_type_encoding_candidate(
    input,
    body_size_proofs=Some(proofs),
  ).unwrap()
  @validate.validate_module(output).unwrap()
  assert_eq(output.type_sec.unwrap().0.length(), 1)
  assert_eq(output.func_sec.unwrap().0, [
    @lib.TypeIdx::new(0U),
    @lib.TypeIdx::new(0U),
  ])
  assert_eq(output.export_sec, input.export_sec)
  assert_eq(
    @binary.encode_module(output).unwrap(),
    @binary.encode_module(expected).unwrap(),
  )
  assert_true(@binary.encode_module(output).unwrap().length() < saved.length())
  let @lib.Func(_, @lib.Expr(body)) = output.code_sec.unwrap().0[1]
  assert_eq(body, [
    @lib.Instruction::local_get(@lib.LocalIdx::new(0U)),
    @lib.Instruction::call(@lib.FuncIdx::new(0U)),
  ])
  assert_eq(@binary.encode_module(input).unwrap(), saved)
  assert_eq(proofs[0], 0)
}