// Licensing scope: see ../../LICENSE and this directory's LICENSE and NOTICE.
// 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.

///|
priv struct Dae2TypeWorkspace {
  mut types : @lib.TypeSec
  mut flat_count : Int32
}

///|
fn Dae2TypeWorkspace::new(types : @lib.TypeSec) -> Dae2TypeWorkspace {
  let @lib.TypeSec(groups) = types
  // Own the outer group vector before appending. Original groups and their
  // immutable definitions retain their identities until final family rewriting.
  {
    types: @lib.TypeSec::new(groups.copy()),
    flat_count: dae_type_sec_flattened_length(types),
  }
}

///|
fn Dae2TypeWorkspace::append_signature(
  self : Dae2TypeWorkspace,
  params : Array[@lib.ValType],
  results : Array[@lib.ValType],
  metadata : @lib.TypeMetadata,
) -> @lib.TypeIdx {
  let index = @lib.TypeIdx::new(self.flat_count.to_uint())
  let @lib.TypeSec(groups) = self.types
  groups.push(
    @lib.RecType::new(
      @lib.SubType::comp_type(@lib.CompType::func(params, results), metadata~),
    ),
  )
  self.flat_count += 1
  index
}

///|
fn Dae2TypeWorkspace::adopt_lower_types(
  self : Dae2TypeWorkspace,
  state : HotPipelineLowerState,
) -> Unit {
  if state.type_sec_changed {
    match state.next_type_sec {
      Some(types) => {
        self.types = types
        self.flat_count = dae_type_sec_flattened_length(types)
      }
      None => abort("DAE2 lowering removed the type section")
    }
  }
}

///|
// Type locations use the same graph as function parameters/results. All
// members of a declared subtype family share liveness, while private direct
// functions keep independent locations and receive replacement signatures.
fn dae2_exposure_module(mod_ : @lib.Module) -> @lib.Module {
  match mod_.elem_sec {
    Some(@lib.ElemSec(elems)) => {
      let retained = elems.filter(fn(elem) {
        let @lib.Elem(mode, kind) = elem
        if !(mode is @lib.Declarative) {
          return true
        }
        // An untyped declaration authorizes ref.func validation but does not
        // create a runtime reference. Real body/global/table references remain
        // visible to the ordinary exposure collector. Concrete typed element
        // declarations retain their signature until their type is remapped too.
        match kind {
          @lib.FuncsElemKind(_) | @lib.FuncExprsElemKind(_) => false
          @lib.TypedExprsElemKind(ref_type, _) =>
            ref_type.get_heap_type() !=
            @lib.HeapType::abs(@lib.AbsHeapType::func())
        }
      })
      mod_.with_elem_sec(@lib.ElemSec::new(retained))
    }
    None => mod_
  }
}

///|
fn dae2_call_info(
  infos : Array[Dae2Function],
  type_infos : Array[Int32],
  func : @ir.HotFunc,
  node : Int32,
) -> Dae2Function {
  infos[dae2_call_info_index(type_infos, func, node)]
}

///|
fn dae2_call_info_index(
  type_infos : Array[Int32],
  func : @ir.HotFunc,
  node : Int32,
) -> Int32 {
  let data = @ir.hot_node_get(func, node)
  dae2_call_info_index_from_data(type_infos, func, data)
}

///|
fn dae2_call_info_index_from_data(
  type_infos : Array[Int32],
  func : @ir.HotFunc,
  data : @ir.HotNode,
) -> Int32 {
  match data.op {
    @ir.Call | @ir.ReturnCall => data.imm0
    _ => {
      let sig = @ir.hot_call_sig_get(func, data.imm0)
      type_infos[dae2_type_slot(sig.type_idx, 0)]
    }
  }
}

///|
fn dae2_type_slot(index : @lib.TypeIdx, base : Int32) -> Int32 {
  match index {
    @lib.TypeIdx(raw) => raw.reinterpret_as_int()
    @lib.RecIdx(raw) => base + raw.reinterpret_as_int()
  }
}

///|
fn dae2_table_slot(index : @lib.TableIdx) -> Int32 {
  match index {
    @lib.TableIdx(raw) => raw.reinterpret_as_int()
  }
}

///|
fn dae2_is_abstract_funcref_table(table_type : @lib.TableType) -> Bool {
  let @lib.TableType(ref_type, _) = table_type
  dae2_is_abstract_funcref_ref_type(ref_type)
}

///|
fn dae2_is_abstract_funcref_ref_type(ref_type : @lib.RefType) -> Bool {
  match ref_type.get_heap_type() {
    @lib.AbsHeapTypeHeapType(@lib.FuncAbsHeapType)
    | @lib.SharedAbsHeapTypeHeapType(@lib.FuncAbsHeapType) => true
    _ => false
  }
}

///|
fn dae2_is_abstract_funcref_global(global_type : @lib.GlobalType) -> Bool {
  match global_type {
    @lib.GlobalType(@lib.RefTypeValType(ref_type), _) =>
      dae2_is_abstract_funcref_ref_type(ref_type)
    _ => false
  }
}

///|
fn dae2_has_host_abstract_funcref_global(
  mod_ : @lib.Module,
  context : @ir.HotModuleContext,
) -> Bool {
  let mut global_import = 0
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) =>
      for import_ in imports {
        match import_ {
          @lib.Import(_, _, @lib.GlobalExternType(global_type)) => {
            if dae2_is_abstract_funcref_global(global_type) {
              return true
            }
            global_import += 1
          }
          _ => ()
        }
      }
    None => ()
  }
  match mod_.export_sec {
    Some(@lib.ExportSec(exports)) =>
      for export_ in exports {
        match export_ {
          @lib.Export(_, @lib.GlobalExternIdx(@lib.GlobalIdx(raw))) => {
            let slot = raw.reinterpret_as_int()
            if slot >= global_import &&
              slot < context.globals.length() &&
              dae2_is_abstract_funcref_global(context.globals[slot]) {
              return true
            }
          }
          _ => ()
        }
      }
    None => ()
  }
  false
}

///|
fn dae2_host_abstract_funcref_tables(
  mod_ : @lib.Module,
  context : @ir.HotModuleContext,
) -> Array[Bool] {
  let visible = Array::make(context.tables.length(), false)
  let mut table_import = 0
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) =>
      for import_ in imports {
        match import_ {
          @lib.Import(_, _, @lib.TableExternType(table_type)) => {
            if table_import < visible.length() &&
              dae2_is_abstract_funcref_table(table_type) {
              visible[table_import] = true
            }
            table_import += 1
          }
          _ => ()
        }
      }
    None => ()
  }
  match mod_.export_sec {
    Some(@lib.ExportSec(exports)) =>
      for export_ in exports {
        match export_ {
          @lib.Export(_, @lib.TableExternIdx(table_idx)) => {
            let slot = dae2_table_slot(table_idx)
            if slot >= 0 &&
              slot < visible.length() &&
              dae2_is_abstract_funcref_table(context.tables[slot]) {
              visible[slot] = true
            }
          }
          _ => ()
        }
      }
    None => ()
  }
  match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) =>
      for func in funcs {
        let @lib.Func(_, @lib.Expr(body)) = func
        dae2_mark_runtime_funcref_table_ingress(body, context.tables, visible)
      }
    None => ()
  }
  visible
}

///|
fn dae2_mark_runtime_funcref_table_ingress(
  instrs : Array[@lib.Instruction],
  tables : Array[@lib.TableType],
  visible : Array[Bool],
) -> Unit {
  for instr in instrs {
    match instr {
      @lib.Block(_, @lib.Expr(body))
      | @lib.Loop(_, @lib.Expr(body))
      | @lib.TryTable(_, _, @lib.Expr(body)) =>
        dae2_mark_runtime_funcref_table_ingress(body, tables, visible)
      @lib.Try(_, @lib.Expr(body), catches, _) => {
        dae2_mark_runtime_funcref_table_ingress(body, tables, visible)
        for catch_ in catches {
          let @lib.Expr(catch_body) = match catch_ {
            @lib.LegacyCatch(_, catch_body) | @lib.LegacyCatchAll(catch_body) =>
              catch_body
          }
          dae2_mark_runtime_funcref_table_ingress(catch_body, tables, visible)
        }
      }
      @lib.If(_, then_body, else_body) => {
        dae2_mark_runtime_funcref_table_ingress(then_body, tables, visible)
        match else_body {
          Some(body) =>
            dae2_mark_runtime_funcref_table_ingress(body, tables, visible)
          None => ()
        }
      }
      @lib.TableSet(table_idx)
      | @lib.TableGrow(table_idx)
      | @lib.TableFill(table_idx)
      | @lib.TableCopy(table_idx, _) => {
        let slot = dae2_table_slot(table_idx)
        if slot >= 0 &&
          slot < tables.length() &&
          dae2_is_abstract_funcref_table(tables[slot]) {
          visible[slot] = true
        }
      }
      _ => ()
    }
  }
}

///|
fn dae2_function_reference_may_reach_table(
  exposure : DaeFunctionExposure,
) -> Bool {
  exposure.ref_func_body ||
  exposure.ref_func_module ||
  exposure.active_element ||
  exposure.passive_element
}

///|
fn dae2_mark_host_table_call_types(
  instrs : Array[@lib.Instruction],
  host_tables : Array[Bool],
  exposed : Array[Bool],
) -> Unit {
  for instr in instrs {
    match instr {
      @lib.Block(_, @lib.Expr(body))
      | @lib.Loop(_, @lib.Expr(body))
      | @lib.TryTable(_, _, @lib.Expr(body)) =>
        dae2_mark_host_table_call_types(body, host_tables, exposed)
      @lib.Try(_, @lib.Expr(body), catches, _) => {
        dae2_mark_host_table_call_types(body, host_tables, exposed)
        for catch_ in catches {
          let @lib.Expr(catch_body) = match catch_ {
            @lib.LegacyCatch(_, catch_body) | @lib.LegacyCatchAll(catch_body) =>
              catch_body
          }
          dae2_mark_host_table_call_types(catch_body, host_tables, exposed)
        }
      }
      @lib.If(_, then_body, else_body) => {
        dae2_mark_host_table_call_types(then_body, host_tables, exposed)
        match else_body {
          Some(body) =>
            dae2_mark_host_table_call_types(body, host_tables, exposed)
          None => ()
        }
      }
      @lib.CallIndirect(type_idx, table_idx)
      | @lib.ReturnCallIndirect(type_idx, table_idx) => {
        let slot = dae2_table_slot(table_idx)
        if slot >= 0 && slot < host_tables.length() && host_tables[slot] {
          dae_mark_used_type_idx(type_idx, exposed)
        }
      }
      _ => ()
    }
  }
}

///|
fn dae2_type_ref_edges(
  graph : Dae2Graph,
  owner : Int32,
  value : @lib.ValType,
  base : Int32,
  embedded_types_root : Array[Int32],
) -> Unit {
  match value {
    @lib.RefTypeValType(ref_type) =>
      match ref_type.get_heap_type() {
        @lib.HeapType(index) => graph.edge(owner, dae2_type_slot(index, base))
        @lib.DefTypeHeapType(_) => {
          // Embedded definitions have no stable module index. Keep the visible
          // closure conservative instead of treating a missing identity as
          // empty. Share one fanout node across all embedded references so E
          // references over T types require E + T edges, not E * T edges.
          if embedded_types_root[0] < 0 {
            let type_count = graph.used.length()
            let root = graph.location()
            embedded_types_root[0] = root
            for index = 0; index < type_count; index = index + 1 {
              graph.edge(root, index)
            }
          }
          graph.edge(owner, embedded_types_root[0])
        }
        _ => ()
      }
    _ => ()
  }
}

///|
fn dae2_type_field_edges(
  graph : Dae2Graph,
  owner : Int32,
  field : @lib.FieldType,
  base : Int32,
  embedded_types_root : Array[Int32],
) -> Unit {
  let @lib.FieldType(storage, _) = field
  match storage {
    @lib.ValTypeStorageType(value) =>
      dae2_type_ref_edges(graph, owner, value, base, embedded_types_root)
    _ => ()
  }
}

///|
fn dae2_prepare_type_locations(
  mod_ : @lib.Module,
  context : @ir.HotModuleContext,
  graph : Dae2Graph,
  infos : Array[Dae2Function],
  exposure : Array[DaeFunctionExposure],
  closed_world : Bool,
  private_elements~ : Array[Bool]? = None,
) -> Array[Int32] {
  let total = context.subtypes.length()
  let bases = Array::make(total, 0)
  match mod_.type_sec {
    Some(@lib.TypeSec(groups)) => {
      let mut offset = 0
      for group in groups {
        let size = match group {
          @lib.SingleRecType(_) => 1
          @lib.GroupRecType(items) => items.length()
        }
        for member_idx = 0; member_idx < size; member_idx = member_idx + 1 {
          bases[offset + member_idx] = offset
        }
        offset += size
      }
    }
    None => ()
  }
  let roots = Array::makei(total, fn(index) { index })
  // Validated Wasm supertypes precede their subtypes. Resolve each family root
  // without depending on hash-map insertion order or destination mutations.
  for index = 0; index < total; index = index + 1 {
    for super_type in context.subtypes[index].super_types() {
      let parent = dae2_type_slot(super_type, bases[index])
      roots[index] = roots[parent]
    }
  }
  let type_infos = Array::make(total, -1)
  for index = 0; index < total; index = index + 1 {
    match context.subtypes[index].get_comptype() {
      @lib.FuncCompType(params, results) => {
        let root = roots[index]
        if type_infos[root] < 0 {
          type_infos[root] = infos.length()
          infos.push(Dae2Function::new(graph, params, results))
        }
        type_infos[index] = type_infos[root]
      }
      _ => ()
    }
  }
  let public_types = Dae2Graph::new()
  for index = 0; index < total; index = index + 1 {
    ignore(public_types.location())
  }
  let embedded_types_root = [-1]
  // Exposing any recursive-group member exposes the identity of the whole
  // group, including siblings with no explicit reference edge between them.
  // A star through the base keeps this closure linear in the group size.
  for index = 0; index < total; index = index + 1 {
    if bases[index] != index {
      public_types.edge(index, bases[index])
      public_types.edge(bases[index], index)
    }
  }
  for index = 0; index < total; index = index + 1 {
    let subtype = context.subtypes[index]
    let base = bases[index]
    for parent in subtype.super_types() {
      public_types.edge(index, dae2_type_slot(parent, base))
    }
    for metadata_type in [subtype.describes_type(), subtype.descriptor_type()] {
      match metadata_type {
        Some(target) => public_types.edge(index, dae2_type_slot(target, base))
        None => ()
      }
    }
    match subtype.get_comptype() {
      @lib.FuncCompType(params, results) =>
        for value in params + results {
          dae2_type_ref_edges(
            public_types, index, value, base, embedded_types_root,
          )
        }
      @lib.StructCompType(fields) =>
        for field in fields {
          dae2_type_field_edges(
            public_types, index, field, base, embedded_types_root,
          )
        }
      @lib.ArrayCompType(field) =>
        dae2_type_field_edges(
          public_types, index, field, base, embedded_types_root,
        )
      @lib.ContCompType(target) => {
        let target = dae2_type_slot(target, base)
        public_types.edge(index, target)
        // Stack switching signatures require separate operand/handler rewrites.
        public_types.observe(target)
      }
    }
  }
  let host_abstract_tables = dae2_host_abstract_funcref_tables(mod_, context)
  let has_host_abstract_table = host_abstract_tables.any(fn(visible) { visible })
  let has_host_abstract_global = dae2_has_host_abstract_funcref_global(
    mod_, context,
  )
  let has_host_callable_parameter = dae2_has_host_callable_parameter(context)
  for index = 0; index < exposure.length(); index = index + 1 {
    let e = exposure[index]
    let type_index = dae2_type_slot(context.func_type_idxs[index], 0)
    if dae_function_exposure_blocks_boundary(e) {
      if !closed_world ||
        e.imported ||
        e.exported ||
        (
          (
            has_host_abstract_table ||
            has_host_abstract_global ||
            has_host_callable_parameter
          ) &&
          dae2_function_reference_may_reach_table(e)
        ) {
        public_types.observe(type_index)
      }
    }
  }
  // Imported/exported tables and globals expose their concrete reference types.
  let exposed = Array::make(total, false)
  match mod_.import_sec {
    Some(@lib.ImportSec(imports)) =>
      for import_ in imports {
        let @lib.Import(_, _, kind) = import_
        match kind {
          @lib.TableExternType(tt) => dae_mark_used_table_type(tt, exposed)
          @lib.GlobalExternType(gt) => dae_mark_used_global_type(gt, exposed)
          @lib.TagExternType(@lib.TagType(tt)) =>
            dae_mark_used_type_idx(tt, exposed)
          _ => ()
        }
      }
    None => ()
  }
  match mod_.export_sec {
    Some(@lib.ExportSec(exports)) =>
      for export_ in exports {
        let @lib.Export(_, kind) = export_
        match kind {
          @lib.TableExternIdx(@lib.TableIdx(raw)) =>
            dae_mark_used_table_type(
              context.tables[raw.reinterpret_as_int()],
              exposed,
            )
          @lib.GlobalExternIdx(@lib.GlobalIdx(raw)) =>
            dae_mark_used_global_type(
              context.globals[raw.reinterpret_as_int()],
              exposed,
            )
          _ => ()
        }
      }
    None => ()
  }
  for tag in context.tags {
    let @lib.TagType(index) = tag
    dae_mark_used_type_idx(index, exposed)
  }
  if has_host_abstract_table {
    match mod_.code_sec {
      Some(@lib.CodeSec(funcs)) =>
        for func in funcs {
          let @lib.Func(_, @lib.Expr(body)) = func
          dae2_mark_host_table_call_types(body, host_abstract_tables, exposed)
        }
      None => ()
    }
  }
  for index = 0; index < total; index = index + 1 {
    if exposed[index] || !closed_world {
      public_types.observe(index)
    }
  }
  public_types.solve()
  // Build boundary edges after the public closure. A private scalar element
  // target can use a new group identity while its original type stays fixed.
  for index = 0; index < exposure.length(); index = index + 1 {
    let e = exposure[index]
    let type_index = dae2_type_slot(context.func_type_idxs[index], 0)
    let type_info = infos[type_infos[type_index]]
    let private_element = match private_elements {
      Some(mask) => {
        mask[index] = mask[index] && !public_types.used[type_index]
        mask[index]
      }
      None => false
    }
    if private_element {
      for location in type_info.param_locations {
        graph.observe(location)
      }
      graph.observe(type_info.result_location)
      // This is the pass-local boundary view. Keep the separate mask so final
      // signature allocation preserves the runtime reference identity.
      e.active_element = false
      e.passive_element = false
    } else if dae_function_exposure_blocks_boundary(e) {
      let info = infos[index]
      for param = 0; param < info.param_locations.length(); param = param + 1 {
        graph.edge(
          info.param_locations[param],
          type_info.param_locations[param],
        )
        graph.edge(
          type_info.param_locations[param],
          info.param_locations[param],
        )
      }
      graph.edge(info.result_location, type_info.result_location)
      graph.edge(type_info.result_location, info.result_location)
    }
  }
  for index = 0; index < total; index = index + 1 {
    if public_types.used[index] && type_infos[index] >= 0 {
      let info = infos[type_infos[index]]
      for location in info.param_locations {
        graph.observe(location)
      }
      graph.observe(info.result_location)
    }
  }
  type_infos
}

///|
fn dae2_owned_signature_params(
  params : Array[@lib.ValType],
  source : Array[@lib.ValType],
) -> Array[@lib.ValType] {
  // Unchanged plans borrow source arrays. A newly materialized signature
  // owns its parameters, as it did before lazy plan construction.
  if physical_equal(params, source) {
    params.copy()
  } else {
    params
  }
}

///|
fn dae2_rewrite_signature_type(
  subtype : @lib.SubType,
  params : Array[@lib.ValType],
  results : Array[@lib.ValType],
) -> @lib.SubType {
  let params = match subtype.get_comptype() {
    @lib.FuncCompType(source, _) => dae2_owned_signature_params(params, source)
    _ => params
  }
  let comp = @lib.CompType::func(params, results)
  match subtype {
    @lib.SubType(final_, supers, metadata, _) =>
      @lib.SubType::new(final_, supers, comp, metadata~)
    @lib.CompTypeSubType(metadata, _) =>
      @lib.SubType::comp_type(comp, metadata~)
  }
}

///|
fn dae2_rewrite_type_section(
  types : @lib.TypeSec,
  graph : Dae2Graph,
  infos : Array[Dae2Function],
  type_infos : Array[Int32],
  signature_changes~ : Array[Bool]? = None,
) -> @lib.TypeSec {
  let @lib.TypeSec(groups) = types
  let mut index = 0
  fn rewrite(subtype : @lib.SubType, current : Int32) -> @lib.SubType {
    if current >= type_infos.length() || type_infos[current] < 0 {
      return subtype
    }
    let info_index = type_infos[current]
    let info = infos[info_index]
    let changed = match signature_changes {
      Some(changes) => changes[info_index]
      // Identity stabilization can observe locations and solve again. Its
      // callers must inspect the current graph rather than a final cache.
      None => dae2_signature_changes(graph, info)
    }
    if !changed {
      return subtype
    }
    match subtype.get_comptype() {
      @lib.FuncCompType(params, results) => {
        let (kept, _) = dae2_parameter_plan(graph, info, params=Some(params))
        dae2_rewrite_signature_type(
          subtype,
          kept,
          if graph.used[info.result_location] {
            results
          } else {
            []
          },
        )
      }
      _ => subtype
    }
  }
  let mut rewritten : Array[@lib.RecType]? = None
  for group_index = 0
      group_index < groups.length()
      group_index = group_index + 1 {
    let group = groups[group_index]
    let next_group = match group {
      @lib.SingleRecType(subtype) => {
        let next = rewrite(subtype, index)
        index += 1
        if physical_equal(next, subtype) {
          group
        } else {
          @lib.RecType::new(next)
        }
      }
      @lib.GroupRecType(members) => {
        let mut next_members : Array[@lib.SubType]? = None
        for slot = 0; slot < members.length(); slot = slot + 1 {
          let next = rewrite(members[slot], index)
          index += 1
          if !physical_equal(next, members[slot]) {
            let rows = match next_members {
              Some(rows) => rows
              None => {
                let rows = members.copy()
                next_members = Some(rows)
                rows
              }
            }
            rows[slot] = next
          }
        }
        match next_members {
          Some(members) => @lib.RecType::group(members)
          None => group
        }
      }
    }
    if !physical_equal(next_group, group) {
      let rows = match rewritten {
        Some(rows) => rows
        None => {
          let rows = groups.copy()
          rewritten = Some(rows)
          rows
        }
      }
      rows[group_index] = next_group
    }
  }
  match rewritten {
    Some(groups) => @lib.TypeSec::new(groups)
    None => types
  }
}

///|
fn dae2_preserve_control_types(
  func : @ir.HotFunc,
  context : @ir.HotModuleContext,
  graph : Dae2Graph,
  infos : Array[Dae2Function],
  type_infos : Array[Int32],
  lower_state : HotPipelineLowerState,
  replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> Unit {
  for node = 0; node < @ir.hot_node_count(func); node = node + 1 {
    if !@ir.hot_node_is_live(func, node) || !@ir.hot_is_control_node(func, node) {
      continue
    }
    let block_type = match
      @ir.hot_type_get(func, @ir.hot_node_type(func, node)) {
      @ir.BlockResult(bt) | @ir.BlockResultResolved(bt, _, _) => bt
      _ => continue
    }
    let old_index = match block_type {
      @lib.TypeIdxBlockType(index) => dae2_type_slot(index, 0)
      _ => continue
    }
    if old_index >= type_infos.length() || type_infos[old_index] < 0 {
      continue
    }
    let info = infos[type_infos[old_index]]
    if info.param_locations.all(fn(location) { graph.used[location] }) &&
      (info.results.is_empty() || graph.used[info.result_location]) {
      continue
    }
    let @lib.FuncType(params, results) = match
      @ir.hot_module_resolve_type_func_type(
        context,
        @lib.TypeIdx::new(old_index.reinterpret_as_uint()),
      ) {
      Some(sig) => sig
      None => continue
    }
    let replacement = match replacements.get(old_index) {
      Some(index) => index
      None => {
        let types = match lower_state.next_type_sec {
          Some(types) => types
          None => continue
        }
        let (types, index) = dae_append_new_func_type(types, params, results)
        lower_state.next_type_sec = Some(types)
        lower_state.type_sec_changed = true
        replacements.set(old_index, index)
        index
      }
    }
    @ir.hot_control_result_type_set(
      func,
      node,
      @ir.hot_type_intern_resolved_block_result(
        func,
        @lib.BlockType::type_idx(replacement),
        params,
        results,
      ),
    )
  }
}

///|
// A successful raw replay keeps complete control signatures. Preserve their
// original types independently of the private families being pruned, as HOT
// replay does. This scan runs only after admission and owns no source arrays.
fn dae2_preserve_raw_control_types(
  body : Array[@lib.Instruction],
  context : @ir.HotModuleContext,
  graph : Dae2Graph,
  infos : Array[Dae2Function],
  type_infos : Array[Int32],
  workspace : Dae2TypeWorkspace,
  replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> Unit {
  fn preserve(block : @lib.BlockType) -> Unit {
    guard block is @lib.TypeIdxBlockType(index) else { return }
    let old_index = dae2_type_slot(index, 0)
    if old_index < 0 ||
      old_index >= type_infos.length() ||
      type_infos[old_index] < 0 ||
      replacements.get(old_index) is Some(_) {
      return
    }
    let info = infos[type_infos[old_index]]
    if !dae2_signature_changes(graph, info) {
      return
    }
    let @lib.FuncType(params, results) = match
      @ir.hot_module_resolve_type_func_type(context, index) {
      Some(signature) => signature
      None => return
    }
    replacements.set(
      old_index,
      workspace.append_signature(params, results, @lib.TypeMetadata::new()),
    )
  }
  fn visit(instructions : Array[@lib.Instruction]) -> Unit {
    for instruction in instructions {
      match instruction {
        @lib.Block(block, @lib.Expr(child))
        | @lib.Loop(block, @lib.Expr(child)) => {
          // Source preorder matches the control-node allocation order in HOT.
          preserve(block)
          visit(child)
        }
        @lib.If(block, yes, no) => {
          preserve(block)
          visit(yes)
          if no is Some(child) {
            visit(child)
          }
        }
        _ => ()
      }
    }
  }
  visit(body)
}

///|
fn dae2_original_block_type(
  block : @lib.BlockType,
  replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> @lib.BlockType {
  if block is @lib.TypeIdxBlockType(index) {
    if replacements.get(dae2_type_slot(index, 0)) is Some(replacement) {
      return @lib.BlockType::type_idx(replacement)
    }
  }
  block
}

///|
fn dae2_original_control_types(
  body : Array[@lib.Instruction],
  replacements : @hashmap.HashMap[Int32, @lib.TypeIdx],
) -> Array[@lib.Instruction] {
  body.map(fn(instruction) {
    match instruction {
      @lib.Block(block, @lib.Expr(body)) =>
        @lib.Instruction::block(
          dae2_original_block_type(block, replacements),
          @lib.Expr::new(dae2_original_control_types(body, replacements)),
        )
      @lib.Loop(block, @lib.Expr(body)) =>
        @lib.Instruction::loop_(
          dae2_original_block_type(block, replacements),
          @lib.Expr::new(dae2_original_control_types(body, replacements)),
        )
      @lib.If(block, yes, no) =>
        @lib.Instruction::if_(
          dae2_original_block_type(block, replacements),
          dae2_original_control_types(yes, replacements),
          no.map(fn(body) { dae2_original_control_types(body, replacements) }),
        )
      @lib.Try(block, @lib.Expr(body), catches, delegate) =>
        @lib.Instruction::try_(
          dae2_original_block_type(block, replacements),
          @lib.Expr::new(dae2_original_control_types(body, replacements)),
          catches.map(fn(catch_) {
            match catch_ {
              @lib.LegacyCatch(tag, @lib.Expr(body)) =>
                @lib.LegacyCatch::tagged(
                  tag,
                  @lib.Expr::new(
                    dae2_original_control_types(body, replacements),
                  ),
                )
              @lib.LegacyCatchAll(@lib.Expr(body)) =>
                @lib.LegacyCatch::all(
                  @lib.Expr::new(
                    dae2_original_control_types(body, replacements),
                  ),
                )
            }
          }),
          delegate~,
        )
      @lib.TryTable(block, catches, @lib.Expr(body)) =>
        @lib.Instruction::try_table(
          dae2_original_block_type(block, replacements),
          catches,
          @lib.Expr::new(dae2_original_control_types(body, replacements)),
        )
      _ => instruction
    }
  })
}

///|
fn dae2_instrs_have_indexed_control(instrs : Array[@lib.Instruction]) -> Bool {
  instrs.any(fn(instr) {
    match instr {
      @lib.Block(bt, @lib.Expr(body))
      | @lib.Loop(bt, @lib.Expr(body))
      | @lib.TryTable(bt, _, @lib.Expr(body)) =>
        bt is @lib.TypeIdxBlockType(_) || dae2_instrs_have_indexed_control(body)
      @lib.If(bt, then_body, else_body) =>
        bt is @lib.TypeIdxBlockType(_) ||
        dae2_instrs_have_indexed_control(then_body) ||
        (match else_body {
          Some(body) => dae2_instrs_have_indexed_control(body)
          None => false
        })
      // Legacy adaptation retains its complete preparation path.
      @lib.Try(_, _, _, _) => true
      _ => false
    }
  })
}

///|
fn dae2_function_needs_rewrite(
  graph : Dae2Graph,
  infos : Array[Dae2Function],
  info : Dae2Function,
) -> Bool {
  dae2_signature_changes(graph, info) ||
  info.call_infos.any(fn(callee) {
    dae2_signature_changes(graph, infos[callee])
  })
}

///|
fn dae2_intrinsic_import_mask(context : @ir.HotModuleContext) -> Array[Bool]? {
  let module_name = @lib.Name::from_string("binaryen-intrinsics")
  let field_name = @lib.Name::from_string("call.without.effects")
  let mut result : Array[Bool]? = None
  for index in 0.. {
        let mask = match result {
          Some(mask) => mask
          None => {
            let mask = Array::make(context.func_imports.length(), false)
            result = Some(mask)
            mask
          }
        }
        mask[index] = true
      }
      _ => ()
    }
  }
  result
}

///|
// Only a concrete forwarding wrapper triggers the broader block cleanup.
// Most final artifacts have none, so preserve the existing lightweight walk.
fn dae2_cleanup_forwarding(
  instrs : Array[@lib.Instruction],
  module_ctx : @ir.HotModuleContext,
) -> (Array[@lib.Instruction], Bool, Bool) {
  let out : Array[@lib.Instruction] = []
  let mut changed = false
  let mut scalar = false
  let visit_body = fn(body) {
    let (body, nested_changed, nested_scalar) = dae2_cleanup_forwarding(
      body, module_ctx,
    )
    changed = changed || nested_changed
    scalar = scalar || nested_scalar
    body
  }
  for instr in instrs {
    let instr = pass_lower_map_children(instr, visit_body)
    match instr {
      @lib.Block(block_type, @lib.Expr(body)) if !body.is_empty() &&
        body.all(fn(instr) { instr is @lib.LocalGet(_) }) => {
        let results = match block_type {
          @lib.ValTypeBlockType(_) => 1
          @lib.TypeIdxBlockType(index) =>
            match @ir.hot_module_resolve_type_func_type(module_ctx, index) {
              Some(@lib.FuncType([], results)) => results.length()
              _ => -1
            }
          _ => -1
        }
        if results == body.length() {
          out.append(body)
          changed = true
          scalar = scalar || results == 1
          continue
        }
      }
      _ => ()
    }
    out.push(instr)
  }
  (out, changed, scalar)
}

///|
fn dae2_cleanup_has_forwarding_candidate(
  instrs : Array[@lib.Instruction],
  module_ctx : @ir.HotModuleContext,
) -> Bool {
  for instr in instrs {
    match instr {
      @lib.Block(block_type, @lib.Expr(body)) => {
        if !body.is_empty() && body.all(fn(item) { item is @lib.LocalGet(_) }) {
          let results = match block_type {
            @lib.ValTypeBlockType(_) => 1
            @lib.TypeIdxBlockType(index) =>
              match @ir.hot_module_resolve_type_func_type(module_ctx, index) {
                Some(@lib.FuncType([], results)) => results.length()
                _ => -1
              }
            _ => -1
          }
          if results == body.length() {
            return true
          }
        }
        if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
          return true
        }
      }
      @lib.Loop(_, @lib.Expr(body)) | @lib.TryTable(_, _, @lib.Expr(body)) =>
        if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
          return true
        }
      @lib.If(_, yes, no) => {
        if dae2_cleanup_has_forwarding_candidate(yes, module_ctx) {
          return true
        }
        match no {
          Some(body) =>
            if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
              return true
            }
          None => ()
        }
      }
      @lib.Try(_, @lib.Expr(body), catches, _) => {
        if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
          return true
        }
        for catch_ in catches {
          let catch_body = match catch_ {
            @lib.LegacyCatch(_, @lib.Expr(body))
            | @lib.LegacyCatchAll(@lib.Expr(body)) => body
          }
          if dae2_cleanup_has_forwarding_candidate(catch_body, module_ctx) {
            return true
          }
        }
      }
      _ => ()
    }
  }
  false
}

///|
// Count-free admissions use constant enum cases, without per-function records.
priv enum Dae2CleanupAdmission {
  NoChange
  Forwarding
  Normalize
  Counts(Array[Int32], Array[Int32])
}

///|
fn Dae2CleanupAdmission::may_change(self : Dae2CleanupAdmission) -> Bool {
  !(self is NoChange)
}

///|
fn Dae2CleanupAdmission::has_forwarding(self : Dae2CleanupAdmission) -> Bool {
  self is Forwarding
}

///|
fn Dae2CleanupAdmission::local_counts(
  self : Dae2CleanupAdmission,
) -> (Array[Int32], Array[Int32])? {
  match self {
    Counts(gets, writes) => Some((gets, writes))
    _ => None
  }
}

///|
fn dae2_cleanup_admit_func(
  func : @lib.Func,
  params : Array[@lib.ValType],
  module_ctx : @ir.HotModuleContext,
) -> Dae2CleanupAdmission {
  let @lib.Func(locals, @lib.Expr(body)) = func
  if dae2_cleanup_has_forwarding_candidate(body, module_ctx) {
    return Dae2CleanupAdmission::Forwarding
  }
  let runs = locals.runs
  for index in 0.. 0 && runs[index - 1].vt == runs[index].vt) {
      return Dae2CleanupAdmission::Normalize
    }
  }
  if locals.length() == 0 {
    return Dae2CleanupAdmission::NoChange
  }
  let count = params.length() + locals.length()
  let start = params.length()
  let gets = Array::make(count, 0)
  let writes = Array::make(count, 0)
  pass_lower_count_locals(body, gets, writes)
  let mut has_single_write = false
  for local_id in start.. Bool {
  dae2_cleanup_admit_func(func, params, module_ctx).may_change()
}

///|
// Match the forwarding walk's body-container ownership when compaction has
// no body edits. Leaf payload arrays keep the existing early-return contract.
fn dae2_cleanup_copy_body_containers(
  body : Array[@lib.Instruction],
) -> Array[@lib.Instruction] {
  body.map(fn(instr) {
    pass_lower_map_children(instr, dae2_cleanup_copy_body_containers)
  })
}

///|
// A retained stack capture stays below every intervening operand. Reject
// control transfers and structured regions; ordinary calls and traps keep
// their original execution point. Resolved calls use the shared type snapshot.
fn dae2_capture_stack_effect(
  instr : @lib.Instruction,
  mod_ : @lib.Module,
  module_ctx : @ir.HotModuleContext,
) -> (Int32, Int32)? {
  if instr is @lib.Unreachable ||
    run_hot_pipeline_raw_simplify_locals_instr_is_structured(instr) ||
    dae_instr_has_branch(instr) {
    return None
  }
  match instr {
    @lib.Call(index) =>
      match @ir.hot_module_resolve_func_type(module_ctx, index) {
        Some(@lib.FuncType(params, results)) =>
          Some((params.length(), results.length()))
        None => None
      }
    @lib.CallIndirect(index, _) | @lib.CallRef(index) =>
      match @ir.hot_module_resolve_type_func_type(module_ctx, index) {
        Some(@lib.FuncType(params, results)) =>
          Some((params.length() + 1, results.length()))
        None => None
      }
    _ => dae_instr_stack_effect(instr, mod_, [])
  }
}

///|
// The optimizing variant removes redundant capture pairs after its cleanup
// pipeline as well: SimplifyLocals may itself lift and materialize a stack
// value. Compact source and generated locals together to make reruns bounded.
fn dae2_cleanup_captures(mod_ : @lib.Module) -> @lib.Module {
  let funcs = match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => funcs
    None => return mod_
  }
  if funcs.is_empty() {
    return mod_
  }
  let params = rl_defined_func_param_cache(mod_)
  let module_ctx = @ir.hot_module_context_from_module(mod_)
  let mut module_env : @validate.Env? = None
  let stack_effects = Some(fn(instr) {
    dae2_capture_stack_effect(instr, mod_, module_ctx)
  })
  let mut output : (Array[@lib.Func], Array[Array[Int32]?], Array[Bool])? = None
  let imported = rl_imported_func_count(mod_)
  for index = 0; index < funcs.length(); index = index + 1 {
    // Classify and consume one body at a time. The local census must not be
    // retained across functions or across a body/signature change.
    let admission = dae2_cleanup_admit_func(
      funcs[index],
      params[index],
      module_ctx,
    )
    if !admission.may_change() {
      continue
    }
    let (bodies, remaps, changed_labels) = match output {
      Some(output) => output
      None => {
        let next = (
          funcs.copy(),
          Array::make(funcs.length(), None),
          Array::make(funcs.length(), false),
        )
        output = Some(next)
        next
      }
    }
    let @lib.Func(locals, @lib.Expr(instrs)) = funcs[index]
    let (input, forwarding_changed, scalar) = if admission.has_forwarding() {
      let (instrs, changed, scalar) = dae2_cleanup_forwarding(
        instrs, module_ctx,
      )
      (@lib.Func::new(locals, @lib.Expr::new(instrs)), changed, scalar)
    } else {
      (funcs[index], false, false)
    }
    let (input, labels_changed) = if scalar {
      let env = match module_env {
        Some(env) => env
        None => {
          let env = @validate.Env::new().with_module(mod_)
          module_env = Some(env)
          env
        }
      }
      let (input, blocks_changed) = pass_lower_cleanup_branchless_blocks(
        input,
        mod_,
        module_env=Some(env),
      )
      (input, forwarding_changed || blocks_changed)
    } else {
      (input, forwarding_changed)
    }
    changed_labels[index] = labels_changed
    // Forwarding wrappers preserve every local access. The broader scalar
    // cleanup can change accesses, so it must invalidate any earlier census.
    let local_counts = if scalar { None } else { admission.local_counts() }
    let (body, mapping) = pass_lower_compact_captures_with_map(
      input,
      @lib.Func::new(@lib.Locals::empty(), @lib.Expr::new([])),
      params[index],
      stack_effects~,
      propagate_parameter_aliases=true,
      reorder_body_locals=true,
      local_name_sec=mod_.name_sec,
      function_abs=imported + index,
      local_counts~,
    )
    let @lib.Func(body_locals, @lib.Expr(compacted)) = body
    let body = if physical_equal(compacted, instrs) {
      // Normalization can return the borrowed source body. Clone only then;
      // capture and alias remapping already produce independent containers.
      @lib.Func::new(
        body_locals,
        @lib.Expr::new(dae2_cleanup_copy_body_containers(compacted)),
      )
    } else {
      body
    }
    bodies[index] = body
    remaps[index] = Some(mapping)
  }
  let (bodies, remaps, changed_labels) = match output {
    Some(output) => output
    None => return mod_
  }
  let next = mod_.with_code_sec(@lib.CodeSec::new(bodies))
  match rl_rewrite_name_sec(mod_.name_sec, imported, remaps) {
    Some(names) => {
      // Removing a control label changes the ordinal of later labels. Keep
      // metadata for unchanged functions; changed functions have no label map.
      let label_names = match names.label_names {
        Some(@lib.IndirectNameMap(entries)) => {
          let retained = entries.filter(fn(entry) {
            let @lib.IndirectNameAssoc(index, _) = entry
            let defined = index.to_int() - imported
            defined < 0 ||
            defined >= changed_labels.length() ||
            !changed_labels[defined]
          })
          if retained.is_empty() {
            None
          } else {
            Some(@lib.IndirectNameMap::new(retained))
          }
        }
        None => None
      }
      next.with_name_sec(
        @lib.NameSec::new(
          module_name=names.module_name,
          func_names=names.func_names,
          local_names=names.local_names,
          label_names~,
          type_names=names.type_names,
          table_names=names.table_names,
          memory_names=names.memory_names,
          global_names=names.global_names,
          elem_names=names.elem_names,
          data_names=names.data_names,
          field_names=names.field_names,
          tag_names=names.tag_names,
        ),
      )
    }
    None => next.without_name_sec()
  }
}

///|
// Capture original indexed control types before lifting normalizes block types.
// The summary belongs to this body/signature snapshot and contains no HOT arena.
fn dae2_collect_control_type_infos(
  body : Array[@lib.Instruction],
  type_infos : Array[Int32],
  out : Array[Int32],
  call_infos~ : Array[Int32]? = None,
  suffix_state~ : Dae2StackSuffixState? = None,
  function_index~ : Int32 = 0,
) -> Bool {
  let mut has_calls = false
  for instruction in body {
    let block_type = match instruction {
      @lib.Block(block_type, @lib.Expr(child))
      | @lib.Loop(block_type, @lib.Expr(child))
      | @lib.TryTable(block_type, _, @lib.Expr(child)) => {
        has_calls = dae2_collect_control_type_infos(
            child,
            type_infos,
            out,
            call_infos~,
            suffix_state~,
            function_index~,
          ) ||
          has_calls
        Some(block_type)
      }
      @lib.If(block_type, yes, no) => {
        has_calls = dae2_collect_control_type_infos(
            yes,
            type_infos,
            out,
            call_infos~,
            suffix_state~,
            function_index~,
          ) ||
          has_calls
        if no is Some(child) {
          has_calls = dae2_collect_control_type_infos(
              child,
              type_infos,
              out,
              call_infos~,
              suffix_state~,
              function_index~,
            ) ||
            has_calls
        }
        Some(block_type)
      }
      @lib.Try(_, @lib.Expr(child), catches, _) => {
        match suffix_state {
          Some(state) =>
            state.admissions[function_index] = state.admissions[function_index] |
              3
          None => ()
        }
        out.push(-1)
        has_calls = dae2_collect_control_type_infos(
            child,
            type_infos,
            out,
            call_infos~,
            suffix_state~,
            function_index~,
          ) ||
          has_calls
        for catch_ in catches {
          let @lib.Expr(child) = match catch_ {
            @lib.LegacyCatch(_, body) | @lib.LegacyCatchAll(body) => body
          }
          has_calls = dae2_collect_control_type_infos(
              child,
              type_infos,
              out,
              call_infos~,
              suffix_state~,
              function_index~,
            ) ||
            has_calls
        }
        None
      }
      @lib.Call(@lib.FuncIdx(raw)) | @lib.ReturnCall(@lib.FuncIdx(raw)) => {
        has_calls = true
        match call_infos {
          Some(callees) => callees.push(raw.reinterpret_as_int())
          None => ()
        }
        None
      }
      @lib.CallIndirect(index, _)
      | @lib.ReturnCallIndirect(index, _)
      | @lib.CallRef(index)
      | @lib.ReturnCallRef(index) => {
        match suffix_state {
          Some(state) =>
            state.admissions[function_index] = state.admissions[function_index] |
              2
          None => ()
        }
        has_calls = true
        match call_infos {
          Some(callees) => {
            let slot = dae2_type_slot(index, 0)
            callees.push(
              if slot >= 0 && slot < type_infos.length() {
                type_infos[slot]
              } else {
                -1
              },
            )
          }
          None => ()
        }
        None
      }
      @lib.Return => {
        match suffix_state {
          Some(state) =>
            state.admissions[function_index] = state.admissions[function_index] |
              2
          None => ()
        }
        None
      }
      _ => None
    }
    match (suffix_state, block_type) {
      (Some(state), Some(block)) => {
        // Bit 1 records structured control; bit 2 excludes static shapes
        // requiring general HOT normalization. Reuse this mandatory scan.
        let unsupported = match block {
          @lib.TypeIdxBlockType(index) =>
            !(instruction is @lib.Loop(_, _) &&
            dae2_stack_suffix_indexed_loop_has_no_results(state.context, index))
          _ => false
        }
        state.admissions[function_index] = state.admissions[function_index] |
          (if unsupported { 3 } else { 1 })
      }
      _ => ()
    }
    if block_type is Some(@lib.TypeIdxBlockType(index)) {
      let slot = dae2_type_slot(index, 0)
      out.push(
        if slot >= 0 && slot < type_infos.length() {
          type_infos[slot]
        } else {
          -1
        },
      )
    }
  }
  has_calls
}

///|
test "DAE2 cleanup normalizes declarations without dropping accessed locals" {
  let input = pass_test_module_from_wat(
    "(module (func (result i32) (local i32 i32) local.get 0 local.get 0 i32.add local.get 1 i32.add local.get 1 i32.add))",
  )
  let @lib.Func(_, expr) = input.code_sec.unwrap().0[0]
  let locals = @lib.Locals::from_decl_runs_preserving_groups([
    @lib.LocalRun::new(0U, @lib.ValType::i64()),
    @lib.LocalRun::new(1U, @lib.ValType::i32()),
    @lib.LocalRun::new(1U, @lib.ValType::i32()),
  ])
  let func = @lib.Func::new(locals, expr)
  let input = input.with_code_sec(@lib.CodeSec::new([func]))
  let admission = dae2_cleanup_admit_func(
    func,
    [],
    @ir.hot_module_context_from_module(input),
  )
  assert_true(admission.may_change())
  assert_false(admission.has_forwarding())
  assert_true(admission.local_counts() is None)
  let saved = @binary.encode_module(input).unwrap()
  let output = dae2_cleanup_captures(input)
  let @lib.Func(locals, next_expr) = output.code_sec.unwrap().0[0]
  assert_eq(locals.runs, [@lib.LocalRun::new(2U, @lib.ValType::i32())])
  assert_eq(next_expr, expr)
  @validate.validate_module(output).unwrap()
  let @lib.Expr(body) = next_expr
  body.push(@lib.Instruction::nop())
  assert_eq(@binary.encode_module(input).unwrap(), saved)
}