// 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.

///|
// Adapt multiple legacy handlers to the HOT try_table representation. Handler
// bodies execute outside the protected region, and catch_ref preserves the
// original exception identity for rethrow. The ordinary single-handler path
// stays in legacy form.
priv struct Dae2LegacyState {
  env : @validate.Env
  types : HotPipelineLowerState
  locals : Array[@lib.ValType]
  params : Int32
  needs_exception_identity : Bool
  mut changed : Bool
}

///|
priv struct Dae2LegacyLabelWorkspace {
  heads : Array[Int32]
  offset_prefixes : Array[Int32]
}

///|
priv struct Dae2LegacyLabels {
  workspace : Dae2LegacyLabelWorkspace
  length : Int32
}

///|
fn Dae2LegacyLabels::root() -> Dae2LegacyLabels {
  { workspace: { heads: [0], offset_prefixes: [0], }, length: 1, }
}

///|
fn Dae2LegacyLabels::push(
  self : Dae2LegacyLabels,
  head : Int32,
  tail_offset : Int32,
) -> Dae2LegacyLabels {
  let index = self.length
  let prefix = self.workspace.offset_prefixes[index - 1] + tail_offset
  if index < self.workspace.heads.length() {
    self.workspace.heads[index] = head
    self.workspace.offset_prefixes[index] = prefix
  } else {
    self.workspace.heads.push(head)
    self.workspace.offset_prefixes.push(prefix)
  }
  { workspace: self.workspace, length: index + 1, }
}

///|
fn Dae2LegacyLabels::resolve(self : Dae2LegacyLabels, depth : Int32) -> Int32 {
  if depth < 0 || depth >= self.length {
    abort("invalid legacy label depth")
  }
  let ancestor = self.length - 1 - depth
  self.workspace.heads[ancestor] +
  self.workspace.offset_prefixes[self.length - 1] -
  self.workspace.offset_prefixes[ancestor]
}

///|
fn Dae2LegacyLabels::frame_count(self : Dae2LegacyLabels) -> Int32 {
  self.length
}

///|
fn dae2_legacy_label(
  labels : Dae2LegacyLabels,
  label : @lib.LabelIdx,
) -> @lib.LabelIdx {
  let @lib.LabelIdx(raw) = label
  @lib.LabelIdx::new(labels.resolve(raw.to_int()).reinterpret_as_uint())
}

///|
fn dae2_legacy_push_labels(labels : Dae2LegacyLabels) -> Dae2LegacyLabels {
  labels.push(0, 1)
}

///|
fn dae2_legacy_block_type(
  state : Dae2LegacyState,
  results : Array[@lib.ValType],
) -> @lib.BlockType {
  match results {
    [] => @lib.BlockType::void_()
    [ty] => @lib.BlockType::val_type(ty)
    _ =>
      @lib.BlockType::type_idx(
        run_hot_pipeline_lower_intern_block_result_type_idx(
          state.types,
          results,
        ),
      )
  }
}

///|
fn dae2_legacy_local(
  state : Dae2LegacyState,
  ty : @lib.ValType,
) -> @lib.LocalIdx {
  let index = state.params + state.locals.length()
  state.locals.push(ty)
  @lib.LocalIdx::new(index.reinterpret_as_uint())
}

///|
fn dae2_legacy_catch(
  catch_ : @lib.Catch,
  labels : Dae2LegacyLabels,
) -> @lib.Catch {
  match catch_ {
    @lib.Catch(tag, label) =>
      @lib.Catch::new(tag, dae2_legacy_label(labels, label))
    @lib.CatchRef(tag, label) =>
      @lib.Catch::ref_(tag, dae2_legacy_label(labels, label))
    @lib.CatchAll(label) => @lib.Catch::all(dae2_legacy_label(labels, label))
    @lib.CatchAllRef(label) =>
      @lib.Catch::all_ref(dae2_legacy_label(labels, label))
  }
}

///|
fn dae2_legacy_handlers(
  handlers : Array[@lib.ResumeHandler],
  labels : Dae2LegacyLabels,
) -> Array[@lib.ResumeHandler] {
  handlers.map(fn(handler) {
    match handler {
      @lib.ResumeOnLabel(tag, label) =>
        @lib.ResumeHandler::on_label(tag, dae2_legacy_label(labels, label))
      @lib.ResumeOnSwitch(_) => handler
    }
  })
}

///|
#valtype
priv struct Dae2LegacyExceptionFrame {
  source_label_count : Int32
  local_id : Int32
}

///|
fn dae2_legacy_body(
  state : Dae2LegacyState,
  instrs : Array[@lib.Instruction],
  labels : Dae2LegacyLabels,
  exceptions : Array[Dae2LegacyExceptionFrame],
) -> Result[Array[@lib.Instruction], String] {
  let out : Array[@lib.Instruction] = []
  for instr in instrs {
    let next = match instr {
      @lib.Instruction::Br(label) =>
        @lib.Instruction::br(dae2_legacy_label(labels, label))
      @lib.Instruction::BrIf(label) =>
        @lib.Instruction::br_if(dae2_legacy_label(labels, label))
      @lib.Instruction::BrTable(targets, fallback) =>
        @lib.Instruction::br_table(
          targets.map(fn(label) { dae2_legacy_label(labels, label) }),
          dae2_legacy_label(labels, fallback),
        )
      @lib.Instruction::BrOnNull(label) =>
        @lib.Instruction::br_on_null(dae2_legacy_label(labels, label))
      @lib.Instruction::BrOnNonNull(label) =>
        @lib.Instruction::br_on_non_null(dae2_legacy_label(labels, label))
      @lib.Instruction::BrOnCast(label, flags, source, target) =>
        @lib.Instruction::br_on_cast(
          dae2_legacy_label(labels, label),
          flags.source_nullable(),
          source,
          flags.target_nullable(),
          target,
          source_exact=flags.source_exact(),
          target_exact=flags.target_exact(),
        )
      @lib.Instruction::BrOnCastFail(label, flags, source, target) =>
        @lib.Instruction::br_on_cast_fail(
          dae2_legacy_label(labels, label),
          flags.source_nullable(),
          source,
          flags.target_nullable(),
          target,
          source_exact=flags.source_exact(),
          target_exact=flags.target_exact(),
        )
      @lib.Instruction::BrOnCastDescEq(label, source, target) =>
        @lib.Instruction::br_on_cast_desc_eq(
          dae2_legacy_label(labels, label),
          source,
          target,
        )
      @lib.Instruction::BrOnCastDescEqFail(label, source, target) =>
        @lib.Instruction::br_on_cast_desc_eq_fail(
          dae2_legacy_label(labels, label),
          source,
          target,
        )
      @lib.Instruction::Block(bt, @lib.Expr(body))
      | @lib.Instruction::Loop(bt, @lib.Expr(body)) => {
        let rewritten = match
          dae2_legacy_body(
            state,
            body,
            dae2_legacy_push_labels(labels),
            exceptions,
          ) {
          Ok(body) => body
          Err(err) => return Err(err)
        }
        if instr is @lib.Instruction::Block(_, _) {
          @lib.Instruction::block(bt, @lib.Expr::new(rewritten))
        } else {
          @lib.Instruction::loop_(bt, @lib.Expr::new(rewritten))
        }
      }
      @lib.Instruction::If(bt, yes, no) => {
        let inner = dae2_legacy_push_labels(labels)
        let yes = match dae2_legacy_body(state, yes, inner, exceptions) {
          Ok(body) => body
          Err(err) => return Err(err)
        }
        let no = match no {
          Some(body) =>
            match dae2_legacy_body(state, body, inner, exceptions) {
              Ok(body) => Some(body)
              Err(err) => return Err(err)
            }
          None => None
        }
        @lib.Instruction::if_(bt, yes, no)
      }
      @lib.Instruction::TryTable(bt, catches, @lib.Expr(body)) => {
        let body = match
          dae2_legacy_body(
            state,
            body,
            dae2_legacy_push_labels(labels),
            exceptions,
          ) {
          Ok(body) => body
          Err(err) => return Err(err)
        }
        @lib.Instruction::try_table(
          bt,
          catches.map(fn(catch_) { dae2_legacy_catch(catch_, labels) }),
          @lib.Expr::new(body),
        )
      }
      @lib.Instruction::Try(bt, @lib.Expr(body), catches, delegate) =>
        if catches.length() <= 1 {
          let inner = dae2_legacy_push_labels(labels)
          let body = match dae2_legacy_body(state, body, inner, exceptions) {
            Ok(body) => body
            Err(err) => return Err(err)
          }
          let next_catches : Array[@lib.LegacyCatch] = []
          // Borrow one catch stack; push/pop avoids copying ancestor rows.
          let handler_exceptions = exceptions
          handler_exceptions.push({
            source_label_count: inner.frame_count(),
            local_id: -1,
          })
          for catch_ in catches {
            let catch_body = match catch_ {
              @lib.LegacyCatch(_, @lib.Expr(body))
              | @lib.LegacyCatchAll(@lib.Expr(body)) => body
            }
            let rewritten = match
              dae2_legacy_body(state, catch_body, inner, handler_exceptions) {
              Ok(body) => body
              Err(err) => return Err(err)
            }
            next_catches.push(
              match catch_ {
                @lib.LegacyCatch(tag, _) =>
                  @lib.LegacyCatch::tagged(tag, @lib.Expr::new(rewritten))
                @lib.LegacyCatchAll(_) =>
                  @lib.LegacyCatch::all(@lib.Expr::new(rewritten))
              },
            )
          }
          ignore(handler_exceptions.pop())
          @lib.Instruction::try_(
            bt,
            @lib.Expr::new(body),
            next_catches,
            delegate=delegate.map(fn(label) { dae2_legacy_label(labels, label) }),
          )
        } else {
          if delegate is Some(_) {
            return Err("dae2: legacy try mixes catches and delegate")
          }
          state.changed = true
          let count = catches.length()
          let (params, results) = match state.env.expand_blocktype(bt) {
            Ok(types) => types
            Err(err) => return Err(err)
          }
          let result_bt = dae2_legacy_block_type(state, results)
          let parameter_locals = params.map(fn(ty) {
            dae2_legacy_local(state, ty)
          })
          for i = parameter_locals.length() - 1; i >= 0; i = i - 1 {
            out.push(@lib.Instruction::local_set(parameter_locals[i]))
          }
          let body_labels = labels.push(count + 1, count + 2)
          let rewritten_body = match
            dae2_legacy_body(state, body, body_labels, exceptions) {
            Ok(body) => body
            Err(err) => return Err(err)
          }
          let protected_body = parameter_locals.map(fn(index) {
            @lib.Instruction::local_get(index)
          })
          protected_body.append(rewritten_body)
          let arms : Array[@lib.Catch] = []
          let payload_types : Array[Array[@lib.ValType]] = []
          for i = 0; i < count; i = i + 1 {
            let target = @lib.LabelIdx::new(i.reinterpret_as_uint())
            let payload = match catches[i] {
              @lib.LegacyCatch(tag, _) => {
                arms.push(
                  if state.needs_exception_identity {
                    @lib.Catch::ref_(tag, target)
                  } else {
                    @lib.Catch::new(tag, target)
                  },
                )
                match state.env.resolve_tag_functype(tag) {
                  Some(@lib.FuncType(params, _)) => params.copy()
                  None => return Err("dae2: invalid legacy catch tag")
                }
              }
              @lib.LegacyCatchAll(_) => {
                arms.push(
                  if state.needs_exception_identity {
                    @lib.Catch::all_ref(target)
                  } else {
                    @lib.Catch::all(target)
                  },
                )
                []
              }
            }
            if state.needs_exception_identity {
              payload.push(@lib.ValType::ref_null_exn())
            }
            payload_types.push(payload)
          }
          let mut nested = [
            @lib.Instruction::try_table(
              result_bt,
              arms,
              @lib.Expr::new(protected_body),
            ),
            @lib.Instruction::br(
              @lib.LabelIdx::new(count.reinterpret_as_uint()),
            ),
          ]
          for i = 0; i < count; i = i + 1 {
            let handler_labels = labels.push(count - 1 - i, count - i)
            let exception = if state.needs_exception_identity {
              Some(dae2_legacy_local(state, @lib.ValType::ref_null_exn()))
            } else {
              None
            }
            let handler_exceptions = exceptions
            handler_exceptions.push({
              source_label_count: handler_labels.frame_count(),
              local_id: match exception {
                Some(index) => index.0.to_int()
                None => -1
              },
            })
            let catch_body = match catches[i] {
              @lib.LegacyCatch(_, @lib.Expr(body))
              | @lib.LegacyCatchAll(@lib.Expr(body)) => body
            }
            let handler_body = match
              dae2_legacy_body(
                state, catch_body, handler_labels, handler_exceptions,
              ) {
              Ok(body) => body
              Err(err) => return Err(err)
            }
            ignore(handler_exceptions.pop())
            let next = [
              @lib.Instruction::block(
                dae2_legacy_block_type(state, payload_types[i]),
                @lib.Expr::new(nested),
              ),
            ]
            if exception is Some(index) {
              next.push(@lib.Instruction::local_set(index))
            }
            next.append(handler_body)
            next.push(
              @lib.Instruction::br(
                @lib.LabelIdx::new((count - 1 - i).reinterpret_as_uint()),
              ),
            )
            nested = next
          }
          @lib.Instruction::block(result_bt, @lib.Expr::new(nested))
        }
      @lib.Instruction::Rethrow(@lib.LabelIdx(depth)) => {
        if depth >= labels.frame_count().reinterpret_as_uint() {
          return Err("invalid legacy rethrow label depth")
        }
        let target_prefix = labels.frame_count() - depth.reinterpret_as_int()
        let mut target = None
        for i = exceptions.length() - 1; i >= 0; i = i - 1 {
          if exceptions[i].source_label_count == target_prefix {
            target = Some(exceptions[i].local_id)
            break
          }
        }
        let local_id = match target {
          Some(id) => id
          None => return Err("legacy rethrow must target an active catch label")
        }
        if local_id >= 0 {
          out.push(
            @lib.Instruction::local_get(
              @lib.LocalIdx::new(local_id.reinterpret_as_uint()),
            ),
          )
          @lib.Instruction::throw_ref()
        } else {
          @lib.Instruction::rethrow(
            dae2_legacy_label(labels, @lib.LabelIdx::new(depth)),
          )
        }
      }
      @lib.Instruction::Resume(ty, handlers) =>
        @lib.Instruction::resume_(ty, dae2_legacy_handlers(handlers, labels))
      @lib.Instruction::ResumeThrow(ty, tag, handlers) =>
        @lib.Instruction::resume_throw(
          ty,
          tag,
          dae2_legacy_handlers(handlers, labels),
        )
      @lib.Instruction::ResumeThrowRef(ty, handlers) =>
        @lib.Instruction::resume_throw_ref(
          ty,
          dae2_legacy_handlers(handlers, labels),
        )
      _ => instr
    }
    out.push(next)
  }
  Ok(out)
}

///|
fn dae2_legacy_unchanged_func(func : @lib.Func) -> @lib.Func {
  let @lib.Func(locals, body) = func
  let runs = locals.runs()
  for index in 0.. 0 && runs[index - 1].vt == runs[index].vt) {
      // Handler adaptation historically canonicalizes declaration groups in
      // every function of a changed module. Preserve those exact bytes while
      // retaining the original instruction span and avoiding expanded locals.
      return @lib.Func::new(@lib.Locals::new(runs), body)
    }
  }
  func
}

///|
#valtype
priv struct Dae2LegacySetup {
  env : @validate.Env
  types : HotPipelineLowerState
  bodies : Array[@lib.Func]
}

///|
fn Dae2LegacySetup::new(mod_ : @lib.Module) -> Dae2LegacySetup {
  {
    env: @validate.Env::new().with_module(mod_),
    types: { next_type_sec: mod_.type_sec, type_sec_changed: false, },
    bodies: [],
  }
}

///|
fn dae2_legacy_signature_param_count(
  idx : @lib.TypeIdx,
  arities : Array[Int32],
  last_group_start : Int32,
) -> Int32 {
  let slot = match idx {
    @lib.TypeIdx(raw) => raw.reinterpret_as_int()
    @lib.RecIdx(raw) => {
      let relative = raw.reinterpret_as_int()
      if relative < 0 || relative >= arities.length() - last_group_start {
        return -1
      }
      last_group_start + relative
    }
  }
  arities.get(slot).unwrap_or(-1)
}

///|
// Match Env::with_module's ordered function table, including its omission of
// unresolved/non-function signatures. This is O(types + declarations), without
// body feature scans or unrelated imported/defined member environments.
fn dae2_legacy_signature_param_counts(mod_ : @lib.Module) -> Array[Int32] {
  let arities : Array[Int32] = []
  let mut last_group_start = 0
  if mod_.type_sec is Some(@lib.TypeSec(groups)) {
    for group in groups {
      last_group_start = arities.length()
      match group {
        @lib.SingleRecType(subtype) =>
          arities.push(
            match subtype.get_comptype() {
              @lib.FuncCompType(params, _) => params.length()
              _ => -1
            },
          )
        @lib.GroupRecType(group) =>
          for subtype in group {
            arities.push(
              match subtype.get_comptype() {
                @lib.FuncCompType(params, _) => params.length()
                _ => -1
              },
            )
          }
      }
    }
  }
  let counts : Array[Int32] = []
  if mod_.import_sec is Some(@lib.ImportSec(imports)) {
    for import_ in imports {
      if import_ is @lib.Import(_, _, @lib.FuncExternType(idx)) {
        let count = dae2_legacy_signature_param_count(
          idx, arities, last_group_start,
        )
        if count >= 0 {
          counts.push(count)
        }
      }
    }
  }
  if mod_.func_sec is Some(@lib.FuncSec(indices)) {
    for idx in indices {
      let count = dae2_legacy_signature_param_count(
        idx, arities, last_group_start,
      )
      if count >= 0 {
        counts.push(count)
      }
    }
  }
  counts
}

///|
fn dae2_prepare_legacy_handlers(
  mod_ : @lib.Module,
  setup_counts~ : Array[Int32]? = None,
) -> Result[@lib.Module, String] {
  let funcs = match mod_.code_sec {
    Some(@lib.CodeSec(funcs)) => funcs
    None => return Ok(mod_)
  }
  if funcs.is_empty() {
    return Ok(mod_)
  }
  let params_by_function = dae2_legacy_signature_param_counts(mod_)
  let mut setup : Dae2LegacySetup? = None
  let imported = dae_imported_func_count(mod_)
  let mut changed = false
  for index = 0; index < funcs.length(); index = index + 1 {
    let @lib.Func(locals, @lib.Expr(body)) = funcs[index]
    let params = match params_by_function.get(index + imported) {
      Some(params) => params
      None => return Err("dae2: missing legacy function type")
    }
    if (dae2_handler_admission(body) & 2) == 0 {
      if setup is Some(owners) {
        owners.bodies.push(dae2_legacy_unchanged_func(funcs[index]))
      }
      continue
    }
    let owners = match setup {
      Some(owners) => owners
      None => {
        if setup_counts is Some(counts) {
          counts[0] += 1
          counts[1] += 1
          counts[2] += 1
        }
        let owners = Dae2LegacySetup::new(mod_)
        for prefix in 0.. body
      Err(err) => return Err(err)
    }
    changed = changed || state.changed
    owners.bodies.push(
      if state.changed {
        @lib.Func::new(
          @lib.Locals::from_types(local_types),
          @lib.Expr::new(body),
        )
      } else {
        dae2_legacy_unchanged_func(funcs[index])
      },
    )
  }
  if !changed {
    return Ok(mod_)
  }
  let owners = setup.unwrap()
  let next = mod_.with_code_sec(@lib.CodeSec::new(owners.bodies))
  let next = match owners.types.next_type_sec {
    Some(types) => next.with_type_sec(types)
    None => next
  }
  match @validate.validate_module(next) {
    Ok(_) => Ok(next)
    Err(err) => Err("dae2: invalid legacy handler adaptation: \{err}")
  }
}

///|
fn dae2_legacy_has_rethrow(instrs : Array[@lib.Instruction]) -> Bool {
  // A nested handler can rethrow an outer exception. Retain all identities in
  // that function; do not infer a handler-local absence from its own body only.
  let work = [instrs]
  while !work.is_empty() {
    for instr in work.pop().unwrap() {
      match instr {
        @lib.Instruction::Rethrow(_) => return true
        @lib.Instruction::Block(_, @lib.Expr(body))
        | @lib.Instruction::Loop(_, @lib.Expr(body))
        | @lib.Instruction::TryTable(_, _, @lib.Expr(body)) => work.push(body)
        @lib.Instruction::If(_, yes, no) => {
          work.push(yes)
          if no is Some(body) {
            work.push(body)
          }
        }
        @lib.Instruction::Try(_, @lib.Expr(body), catches, _) => {
          work.push(body)
          for catch_ in catches {
            match catch_ {
              @lib.LegacyCatch(_, @lib.Expr(body))
              | @lib.LegacyCatchAll(@lib.Expr(body)) => work.push(body)
            }
          }
        }
        _ => ()
      }
    }
  }
  false
}

///|
fn dae2_known_local_throw_target(
  block_type : @lib.BlockType,
  catches : Array[@lib.Catch],
  body : Array[@lib.Instruction],
  env : @validate.Env,
  imported_tags : Int32,
) -> @lib.LabelIdx? {
  let thrown = match body.last() {
    Some(@lib.Throw(tag)) => tag
    _ => return None
  }
  let @lib.TagIdx(raw_tag) = thrown
  // Two imported tags can denote the same exception identity. A locally
  // defined tag is fresh, so declaration-order matching by index is exact.
  if raw_tag.to_int() < imported_tags ||
    env.resolve_tag_functype(thrown) is None {
    return None
  }
  match env.expand_blocktype(block_type) {
    Ok((params, _)) if params.is_empty() => ()
    _ => return None
  }
  for index = 0; index + 1 < body.length(); index = index + 1 {
    match body[index] {
      @lib.Nop
      | @lib.I32Const(_)
      | @lib.I64Const(_)
      | @lib.F32Const(_)
      | @lib.F64Const(_)
      | @lib.RefNull(_)
      | @lib.RefFunc(_)
      | @lib.LocalGet(_)
      | @lib.GlobalGet(_) => ()
      // In particular, a call can raise an exception before the final throw.
      // Removing the protected region would change that earlier dispatch.
      _ => return None
    }
  }
  for catch_ in catches {
    match catch_ {
      @lib.Catch(tag, target) if tag == thrown => return Some(target)
      @lib.CatchRef(tag, _) if tag == thrown => return None
      @lib.CatchAll(target) => return Some(target)
      @lib.CatchAllRef(_) => return None
      _ => ()
    }
  }
  None
}

///|
fn dae2_fold_known_local_throws_body(
  body : Array[@lib.Instruction],
  env : @validate.Env,
  imported_tags : Int32,
) -> (Array[@lib.Instruction], Bool) {
  let out : Array[@lib.Instruction] = []
  let mut changed = false
  for instr in body {
    let instr = pass_lower_map_children(instr, fn(children) {
      let (next, child_changed) = dae2_fold_known_local_throws_body(
        children, env, imported_tags,
      )
      changed = changed || child_changed
      next
    })
    match instr {
      @lib.TryTable(bt, catches, @lib.Expr(protected_body)) =>
        match
          dae2_known_local_throw_target(
            bt, catches, protected_body, env, imported_tags,
          ) {
          Some(target) => {
            for index = 0
                index + 1 < protected_body.length()
                index = index + 1 {
              out.push(protected_body[index])
            }
            // Catch targets are relative to the surrounding scope. Removing
            // the try_table therefore makes that same depth an ordinary br.
            // A catch-all target takes no payload; br discards excess values.
            // Reference-capturing handlers are excluded by the target proof.
            out.push(@lib.Instruction::br(target))
            changed = true
            continue
          }
          None => ()
        }
      _ => ()
    }
    out.push(instr)
  }
  (out, changed)
}

///|
fn dae2_body_has_exception_or_continuation(
  body : Array[@lib.Instruction],
) -> Bool {
  for instr in body {
    match instr {
      @lib.Try(_, _, _, _)
      | @lib.TryTable(_, _, _)
      | @lib.Throw(_)
      | @lib.ThrowRef
      | @lib.Rethrow(_)
      | @lib.Suspend(_)
      | @lib.Resume(_, _)
      | @lib.ResumeThrow(_, _, _)
      | @lib.ResumeThrowRef(_, _)
      | @lib.StackSwitch(_, _) => return true
      @lib.Block(_, @lib.Expr(children)) | @lib.Loop(_, @lib.Expr(children)) =>
        if dae2_body_has_exception_or_continuation(children) {
          return true
        }
      @lib.If(_, yes, no) => {
        if dae2_body_has_exception_or_continuation(yes) {
          return true
        }
        if no is Some(no) && dae2_body_has_exception_or_continuation(no) {
          return true
        }
      }
      _ => ()
    }
  }
  false
}

///|
// Body-scoped admission only: bit 1 identifies a possible known throw fold,
// bit 2 a legacy multi-handler adaptation. All nested handler bodies matter.
// Signature and tag proofs remain in the admitted transformations.
fn dae2_handler_admission(body : Array[@lib.Instruction]) -> Int32 {
  let mut flags = 0
  for instr in body {
    match instr {
      @lib.TryTable(_, _, @lib.Expr(child)) => {
        if child.last() is Some(@lib.Throw(_)) {
          flags = flags | 1
        }
        flags = flags | dae2_handler_admission(child)
      }
      @lib.Block(_, @lib.Expr(child)) | @lib.Loop(_, @lib.Expr(child)) =>
        flags = flags | dae2_handler_admission(child)
      @lib.If(_, yes, no) => {
        flags = flags | dae2_handler_admission(yes)
        if no is Some(child) {
          flags = flags | dae2_handler_admission(child)
        }
      }
      @lib.Try(_, @lib.Expr(child), catches, _) => {
        if catches.length() > 1 {
          flags = flags | 2
        }
        flags = flags | dae2_handler_admission(child)
        for catch_ in catches {
          match catch_ {
            @lib.LegacyCatch(_, @lib.Expr(child))
            | @lib.LegacyCatchAll(@lib.Expr(child)) =>
              flags = flags | dae2_handler_admission(child)
          }
        }
      }
      _ => ()
    }
  }
  flags
}

///|
fn dae2_fold_known_local_throws(
  mod_ : @lib.Module,
  setup_builds~ : Array[Int32]? = None,
) -> @lib.Module {
  // The fold requires a fresh defined tag. Imported tags can share identity;
  // with no definitions, unresolved local-tag indices cannot pass its proof.
  match mod_.tag_sec {
    Some(@lib.TagSec(tags)) if !tags.is_empty() => ()
    _ => return mod_
  }
  let bodies = match mod_.code_sec {
    Some(@lib.CodeSec(bodies)) => bodies
    None => return mod_
  }
  match setup_builds {
    Some(builds) => builds[0] += 1
    None => ()
  }
  let env = @validate.Env::new().with_module(mod_)
  let imported_tags = @ir.hot_module_context_from_module(mod_).imported_tag_count
  let mut any_changed = false
  let next = bodies.map(fn(func) {
    let @lib.Func(locals, @lib.Expr(body)) = func
    if (dae2_handler_admission(body) & 1) == 0 {
      return func
    }
    let (body, changed) = dae2_fold_known_local_throws_body(
      body, env, imported_tags,
    )
    if !changed {
      return func
    }
    any_changed = true
    let body = if changed && !dae2_body_has_exception_or_continuation(body) {
      // Remove unused void scaffolding only after dispatch folding has
      // eliminated exception scopes. Preserve every result block type.
      run_hot_pipeline_raw_vacuum_flatten_unbranched_result_blocks(
        body,
        true,
        false,
        flatten_results=false,
      ).0
    } else {
      body
    }
    @lib.Func::new(locals, @lib.Expr::new(body))
  })
  if any_changed {
    mod_.with_code_sec(@lib.CodeSec::new(next))
  } else {
    mod_
  }
}

///|
test "DAE2 tagless throw fold keeps bytes without building module setup" {
  let input = pass_test_module_from_wat(
    "(module (func (param i32) (result i32) block (result i32) local.get 0 end))",
  )
  let saved = @binary.encode_module(input).unwrap()
  let setups = [0]
  let output = dae2_fold_known_local_throws(input, setup_builds=Some(setups))
  assert_true(physical_equal(output, input))
  assert_eq(@binary.encode_module(output).unwrap(), saved)
  @validate.validate_module(output).unwrap()
  assert_eq(@binary.encode_module(input).unwrap(), saved)
  assert_eq(setups, [0])
}