// WASM to IR Translator
// Converts WebAssembly stack-based instructions to SSA-form IR

///|
/// Translator state for converting a single WASM function to IR
priv struct Translator {
  builder : FunctionBuilder
  // Value stack - simulates WASM's operand stack
  value_stack : Array[Value]
  // Local variables - mutable in WASM, need SSA tracking
  locals : Array[Value]
  // Block stack for control flow
  block_stack : Array[BlockFrame]
  // Function types from the module
  func_types : Array[@types.FuncType]
  // Type indices for functions
  func_type_indices : Array[Int]
  // Number of imported functions
  num_imports : Int
  // Type indices for imported functions
  import_func_type_indices : Array[Int]
  // Unreachable code flag - set after br/return/unreachable
  // When true, skip translating instructions until block end
  mut is_unreachable : Bool
  // Return continuation block - lazily created when br/br_table jumps to function level
  mut return_continuation : Block?
  // Function result types - needed for lazy return continuation creation
  func_result_types : Array[Type]
  // Memory max pages limit (None = no limit)
  memory_max : Int?
  // Memory is 64-bit indexed (for memory64 proposal)
  // memory_is_64[i] = true if memory i uses 64-bit addresses
  memory_is_64 : Array[Bool]
  // Table sizes for bounds checking
  // table_sizes[i] = number of elements in table i
  table_sizes : Array[Int]
  // Number of wasm function parameters (excludes vmctx params)
  // Used to distinguish params from locals in self.locals array
  num_wasm_params : Int
  // Cross-module support: base index for this module's functions
  // All function indices are offset by this value (default 0)
  func_base : Int
  // Cross-module support: maps local import indices to global indices
  // If empty, uses local indices directly
  import_remap : Array[Int]
  // Module composite types (struct/array/func types) for GC support
  module_types : Array[@types.SubType]
  // VMContext value (vmctx parameter) for accessing runtime state
  vmctx : Value
  // Function environment for desugaring Wasm operations to IR primitives
  func_env : FuncEnvironment
  // Exception tags for catch handler dispatch
  tags : Array[@types.TagType]
  // Depth of nested try_table blocks - when > 0, need to spill locals before calls
  mut try_table_depth : Int
  // Active try_table handler IDs in lexical nesting order.
  // Needed to unwind handlers before tail calls (`return_call*`).
  try_handler_stack : Array[Int]
  // Table is 64-bit indexed (for table64 proposal)
  // table_is_64[i] = true if table i uses 64-bit indices
  table_is_64 : Array[Bool]
  // MilkIR carrier type for each table element.
  table_elem_types : Array[Type]
}

///|
/// Block frame for tracking control flow constructs
priv struct BlockFrame {
  block : Block // The continuation block
  result_types : Array[Type]
  // Local indices the block carries as parameters, in parameter order, from
  // `region_local_params`. A branch to this frame supplies one argument per
  // entry after the explicit results.
  local_params : Array[Int]
  // Stack height at block entry
  stack_height : Int
  mut has_predecessor : Bool
}

///|
/// Immutable public translation context for Wasm-to-MilkIR frontend lowering.
struct TranslationContext {
  func_types : Array[@types.FuncType]
  func_type_indices : Array[Int]
  num_imports : Int
  import_func_type_indices : Array[Int]
  embedding_env : @embedding.EmbeddingEnvironment
  memory_max : Int?
  memory_is_64 : Array[Bool]
  memory_page_size_log2 : Array[Int]
  tables : Array[@types.Table]
  global_types : Array[@types.GlobalType]
  type_rec_groups : Array[Int]
  func_base : Int
  import_remap : Array[Int]
  module_types : Array[@types.SubType]
  tags : Array[@types.TagType]
  memory_mins : Array[Int64]
  memory_count : Int
  data_count : Int
  element_types : Array[@types.ValueType]
}

///|
/// Build frontend translation metadata from a decoded Wasm module.
pub fn TranslationContext::from_module(
  mod_ : @types.Module,
  embedding_env : @embedding.EmbeddingEnvironment,
  memory_max_override? : Int? = None,
  func_base? : Int = 0,
  import_remap? : Array[Int] = [],
) -> TranslationContext {
  let mut num_imports = 0
  let import_func_type_indices : Array[Int] = []
  let global_types : Array[@types.GlobalType] = []
  let tags : Array[@types.TagType] = []
  let tables : Array[@types.Table] = []
  let mut memory_count = mod_.memories.length()
  for imp in mod_.imports {
    match imp.desc {
      Func(type_idx) => {
        num_imports = num_imports + 1
        import_func_type_indices.push(type_idx)
      }
      Table(table_type) => tables.push({ type_: table_type, init: None })
      Global(global_type) => global_types.push(global_type)
      Tag(tag_type_idx) => tags.push({ type_idx: tag_type_idx })
      Memory(_) => memory_count = memory_count + 1
    }
  }
  for global in mod_.globals {
    global_types.push(global.type_)
  }
  for tag in mod_.tags {
    tags.push(tag)
  }
  for table in mod_.tables {
    tables.push(table)
  }
  let memory_max : Int? = match memory_max_override {
    Some(_) => memory_max_override
    None =>
      if mod_.memories.length() > 0 {
        mod_.memories[0].limits.max.map(fn(m) { m.to_int() })
      } else {
        None
      }
  }
  {
    func_types: module_func_type_table(mod_.types),
    func_type_indices: mod_.funcs,
    num_imports,
    import_func_type_indices,
    embedding_env,
    memory_max,
    memory_is_64: mod_.memories.map(fn(m) { m.is_memory64 }),
    memory_page_size_log2: mod_.memories.map(fn(m) { m.page_size_log2 }),
    tables,
    global_types,
    type_rec_groups: mod_.type_rec_groups,
    func_base,
    import_remap,
    module_types: mod_.types,
    tags,
    memory_mins: mod_.memories.map(fn(m) {
      m.limits.min * (1L << m.page_size_log2)
    }),
    memory_count,
    data_count: mod_.datas.length(),
    element_types: mod_.elems.map(fn(element) { element.type_ }),
  }
}

///|
/// Build the frontend's function-type lookup table while preserving module
/// type indices used by `call_indirect` and typed references.
fn module_func_type_table(
  subtypes : Array[@types.SubType],
) -> Array[@types.FuncType] {
  let result : Array[@types.FuncType] = []
  for subtype in subtypes {
    match subtype.composite {
      Func(ft) => result.push(ft)
      _ => result.push({ params: [], results: [] })
    }
  }
  result
}

///|
fn get_module_func_name(
  mod_ : @types.Module,
  func_idx : Int,
  name? : String,
) -> String {
  if name is Some(n) {
    n
  } else {
    match mod_.func_names.get(func_idx) {
      Some(n) => n
      None => {
        let mut export_name = ""
        for exp in mod_.exports {
          if exp.desc is Func(idx) && idx == func_idx {
            export_name = exp.name
            break
          }
        }
        if export_name == "" {
          "func_\{func_idx}"
        } else {
          export_name
        }
      }
    }
  }
}

///|
fn Translator::mark_block_frame_reachable(self : Translator, idx : Int) -> Unit {
  if idx >= 0 && idx < self.block_stack.length() {
    self.block_stack[idx].has_predecessor = true
  }
}

///|
/// Low-level constructor with explicit parameters.
/// Prefer `TranslationContext`-based public APIs for normal use.
fn Translator::Translator(
  name : String,
  func_type : @types.FuncType,
  locals : Array[@types.ValueType],
  func_types : Array[@types.FuncType],
  func_type_indices : Array[Int],
  num_imports : Int,
  import_func_type_indices : Array[Int],
  embedding_env : @embedding.EmbeddingEnvironment,
  memory_max? : Int? = None,
  memory_is_64? : Array[Bool] = [],
  memory_page_size_log2? : Array[Int] = [],
  tables? : Array[@types.Table] = [],
  global_types? : Array[@types.GlobalType] = [],
  type_rec_groups? : Array[Int] = [],
  func_base? : Int = 0,
  import_remap? : Array[Int] = [],
  module_types? : Array[@types.SubType] = [],
  tags? : Array[@types.TagType] = [],
  memory_mins? : Array[Int64] = [],
) -> Translator {
  type_rec_groups |> ignore // Reserved for future canonical type indices
  let builder = FunctionBuilder::FunctionBuilder(name)
  // Note: add vmctx as explicit param (Cranelift-style special param)
  // - params[0] = vmctx (X0)
  // This is referenced for desugaring global/table/memory operations
  let vmctx = builder.add_param(I64)

  // Create function environment for desugaring Wasm operations
  let func_env = FuncEnvironment::FuncEnvironment(
    embedding_env,
    global_types,
    memory_mins~,
    memory_is_64~,
    memory_page_size_log2~,
  )

  // Add wasm function parameters (starting from params[1])
  let local_values : Array[Value] = []
  for param in func_type.params {
    let p = builder.add_param(type_from_wasm(param))
    local_values.push(p)
  }
  // Add result types
  let func_result_types : Array[Type] = []
  for result in func_type.results {
    builder.add_result(type_from_wasm(result))
    func_result_types.push(type_from_wasm(result))
  }
  // Initialize locals with zero values - we'll handle them specially
  for local_ty in locals {
    // Create placeholder values for locals
    // In SSA, we need to track the current value of each local
    local_values.push(
      builder.get_function().new_value(type_from_wasm(local_ty)),
    )
  }
  // Calculate table base offsets for multi-table support
  // All tables are flattened into a single indirect_table at runtime
  let table_sizes : Array[Int] = []
  let table_is_64 : Array[Bool] = []
  let table_elem_types : Array[Type] = []
  for table in tables {
    let size = table.type_.limits.min.to_int() // Tables always use 32-bit limits
    table_sizes.push(size)
    table_is_64.push(table.type_.is_table64)
    table_elem_types.push(type_from_wasm(table.type_.elem_type))
  }
  {
    builder,
    value_stack: [],
    locals: local_values,
    block_stack: [],
    func_types,
    func_type_indices,
    num_imports,
    import_func_type_indices,
    is_unreachable: false,
    return_continuation: None,
    func_result_types,
    memory_max,
    memory_is_64,
    table_sizes,
    table_is_64,
    table_elem_types,
    num_wasm_params: func_type.params.length(),
    func_base,
    import_remap,
    module_types,
    vmctx,
    func_env,
    tags,
    try_table_depth: 0,
    try_handler_stack: [],
  }
}

///|
/// Translate a function body from a module.
pub fn translate_function(
  mod_ : @types.Module,
  func_local_idx : Int,
  embedding_env : @embedding.EmbeddingEnvironment,
  name? : String,
  memory_max_override? : Int? = None,
) -> Function {
  let ctx = TranslationContext::from_module(
    mod_,
    embedding_env,
    memory_max_override~,
  )
  let func_idx = ctx.num_imports + func_local_idx
  let func_name = get_module_func_name(mod_, func_idx, name?)
  let type_idx = mod_.funcs[func_local_idx]
  let func_type = mod_.validated_func_type_at(type_idx)
  let code = mod_.codes[func_local_idx]
  translate_function_body(ctx, func_name, func_type, code.locals, code.body)
}

///|
/// Translate a decoded Wasm function body using a frontend translation context.
pub fn translate_function_body(
  ctx : TranslationContext,
  name : String,
  func_type : @types.FuncType,
  locals : Array[@types.ValueType],
  body : Array[@types.Instruction],
) -> Function {
  let translator = Translator::Translator(
    name,
    func_type,
    locals,
    ctx.func_types,
    ctx.func_type_indices,
    ctx.num_imports,
    ctx.import_func_type_indices,
    ctx.embedding_env,
    memory_max=ctx.memory_max,
    memory_is_64=ctx.memory_is_64,
    memory_page_size_log2=ctx.memory_page_size_log2,
    tables=ctx.tables,
    global_types=ctx.global_types,
    type_rec_groups=ctx.type_rec_groups,
    func_base=ctx.func_base,
    import_remap=ctx.import_remap,
    module_types=ctx.module_types,
    tags=ctx.tags,
    memory_mins=ctx.memory_mins,
  )
  translator.translate(body)
}

///|
/// Convert a local value to i64 words for exception spilling.
///
/// Most locals spill as a single i64 word, but v128 spills as two i64 lanes.
fn Translator::local_to_spill_words(
  self : Translator,
  loc_val : Value,
) -> Array[Value] {
  match loc_val.ty {
    I32 => [self.builder.uextend(I64, loc_val)]
    I64 | Ptr | Ref | CallableRef | OpaqueRef => [loc_val] // refs are already word-sized
    F32 => {
      let bits32 = self.builder.bitcast(I32, loc_val)
      [self.builder.uextend(I64, bits32)]
    }
    F64 => [self.builder.bitcast(I64, loc_val)]
    V128 =>
      [
        self.builder.v128_extract64(loc_val, 0),
        self.builder.v128_extract64(loc_val, 1),
      ]
  }
}

///|
/// Convert i64 bits back to the original local type (non-v128).
fn Translator::i64_bits_to_local(
  self : Translator,
  bits : Value,
  target_ty : Type,
) -> Value {
  match target_ty {
    I32 => self.builder.ireduce(I32, bits)
    I64 => bits
    Ptr | Ref | CallableRef | OpaqueRef => self.builder.bitcast(target_ty, bits)
    F32 => {
      let bits32 = self.builder.ireduce(I32, bits)
      self.builder.bitcast(F32, bits32)
    }
    F64 => self.builder.bitcast(F64, bits)
    V128 => abort("internal error: use v128 spill restore path")
  }
}

///|
/// Restore a v128 local from two i64 spill words.
fn Translator::spill_words_to_v128(
  self : Translator,
  lo : Value,
  hi : Value,
) -> Value {
  let zero = self.builder.iconst(I64, 0L)
  let v0 = self.builder.v128_splat64(zero)
  let v1 = self.builder.v128_replace64(v0, lo, 0)
  self.builder.v128_replace64(v1, hi, 1)
}

///|
/// Read an exception payload back into the tag's declared types.
///
/// The inverse of the `local_to_spill_words` walk the throw side runs: the
/// payload is a flat run of i64 words in which a v128 occupies two, so the
/// word index only tracks the value index when no vector precedes it. Both
/// sides derive the layout from the same tag signature, which is what keeps
/// them in step.
fn Translator::read_exception_payload(
  self : Translator,
  tag_types : Array[@types.ValueType],
) -> Array[Value] {
  let args : Array[Value] = []
  let mut word = 0
  for ty in tag_types {
    match ty {
      V128 => {
        let lo = @wasm_milkir.get_exception_value(self.builder, word)
        let hi = @wasm_milkir.get_exception_value(self.builder, word + 1)
        word += 2
        args.push(self.spill_words_to_v128(lo, hi))
      }
      _ => {
        let bits = @wasm_milkir.get_exception_value(self.builder, word)
        word += 1
        // Exhaustive over the IR types, unlike the bitcast catch-all this
        // replaces, which would happily convert a word to anything asked of
        // it -- including v128, which is how ISS-400 reached the verifier.
        args.push(self.i64_bits_to_local(bits, type_from_wasm(ty)))
      }
    }
  }
  args
}

///|
/// Spill all locals before instructions that might throw.
/// Called when try_table_depth > 0 and before Call/CallIndirect/CallRef/Throw/ThrowRef.
fn Translator::spill_locals_if_in_try(self : Translator) -> Unit {
  if self.try_table_depth > 0 {
    let locals_words : Array[Value] = []
    for loc in self.locals {
      for w in self.local_to_spill_words(loc) {
        locals_words.push(w)
      }
    }
    @wasm_milkir.spill_locals_for_throw(self.builder, locals_words)
  }
}

///|
/// Tail calls leave the current function frame immediately, so active
/// try_table handlers in this function must be popped first.
fn Translator::unwind_try_handlers_for_tail_call(self : Translator) -> Unit {
  let mut i = self.try_handler_stack.length()
  while i > 0 {
    i = i - 1
    @wasm_milkir.try_table_end(self.builder, self.try_handler_stack[i])
  }
}

///|
/// Get the number of fields in a struct type
fn Translator::get_struct_field_count(self : Translator, type_idx : Int) -> Int {
  if type_idx >= self.module_types.length() {
    abort("Invalid type index: \{type_idx}")
  }
  let subtype = self.module_types[type_idx]
  match subtype.composite {
    Struct(st) => st.fields.length()
    _ => abort("Type \{type_idx} is not a struct type")
  }
}

///|
/// Get the IR type for an array element
fn Translator::get_array_element_ir_type(
  self : Translator,
  type_idx : Int,
) -> Type {
  if type_idx >= self.module_types.length() {
    // Default to I64 for unknown types
    return I64
  }
  let subtype = self.module_types[type_idx]
  match subtype.composite {
    Array(arr) =>
      match arr.element.storage_type {
        Val(vt) => type_from_wasm(vt)
        // Packed types (i8, i16) are extended to i32
        Packed(_) => I32
      }
    // Not an array type, default to I64
    _ => I64
  }
}

///|
/// Get the IR type for a struct field
fn Translator::get_struct_field_ir_type(
  self : Translator,
  type_idx : Int,
  field_idx : Int,
) -> Type {
  if type_idx >= self.module_types.length() {
    return I64
  }
  let subtype = self.module_types[type_idx]
  match subtype.composite {
    Struct(st) => {
      if field_idx >= st.fields.length() {
        return I64
      }
      match st.fields[field_idx].storage_type {
        Val(vt) => type_from_wasm(vt)
        // Packed types (i8, i16) are extended to i32
        Packed(_) => I32
      }
    }
    _ => I64
  }
}

///|
/// Get the byte width for a packed struct field (for sign/zero extension)
/// Returns 1 for i8, 2 for i16, 0 for non-packed types
fn Translator::get_struct_field_byte_width(
  self : Translator,
  type_idx : Int,
  field_idx : Int,
) -> Int {
  if type_idx >= self.module_types.length() {
    return 0
  }
  let subtype = self.module_types[type_idx]
  match subtype.composite {
    Struct(st) => {
      if field_idx >= st.fields.length() {
        return 0
      }
      match st.fields[field_idx].storage_type {
        Val(_) => 0 // Not a packed type
        Packed(packed) =>
          match packed {
            I8 => 1
            I16 => 2
          }
      }
    }
    _ => 0
  }
}

///|
/// Get the byte width for a packed array element (for sign/zero extension)
/// Returns 1 for i8, 2 for i16, 0 for non-packed types
fn Translator::get_array_element_byte_width(
  self : Translator,
  type_idx : Int,
) -> Int {
  if type_idx >= self.module_types.length() {
    return 0
  }
  let subtype = self.module_types[type_idx]
  match subtype.composite {
    Array(arr) =>
      match arr.element.storage_type {
        Val(_) => 0 // Not a packed type
        Packed(packed) =>
          match packed {
            I8 => 1
            I16 => 2
          }
      }
    _ => 0
  }
}

///|
/// Extract type index from a ValueType (for GC reference types)
fn Translator::extract_type_idx(
  _self : Translator,
  value_type : @types.ValueType,
) -> Int {
  // Abstract types are encoded as negative indices:
  // -1 = anyref (any), -2 = eqref (eq), -3 = i31ref
  // -4 = structref (abstract), -5 = arrayref (abstract)
  // -6 = funcref, -7 = externref
  // -8 = nullref, -9 = nofunc, -10 = noextern
  match value_type {
    // Concrete struct types (idx >= 0) or abstract struct (-1 -> -4)
    RefStruct(idx) | RefNullStruct(idx) => if idx < 0 { -4 } else { idx }
    // Concrete array types (idx >= 0) or abstract array (-1 -> -5)
    RefArray(idx) | RefNullArray(idx) => if idx < 0 { -5 } else { idx }
    RefFuncTyped(idx) | RefNullFuncTyped(idx) => idx
    AnyRef | RefAny => -1
    RefEq | RefNullEq => -2
    RefI31 | RefNullI31 => -3
    FuncRef | RefFunc => -6
    ExternRef | RefExtern => -7
    NullRef => -8
    NullFuncRef => -9
    NullExternRef => -10
    // For value types or unsupported ref types, return -100 as error marker
    _ => -100
  }
}

///|
/// Push a value onto the operand stack
fn Translator::push(self : Translator, v : Value) -> Unit {
  self.value_stack.push(v)
}

///|
/// Pop a value from the operand stack
fn Translator::pop(self : Translator) -> Value {
  match self.value_stack.pop() {
    Some(v) => v
    None => abort("Stack underflow")
  }
}

///|
/// Peek at the top of the stack
fn Translator::peek(self : Translator) -> Value {
  match self.value_stack.last() {
    Some(v) => v
    None => abort("Stack underflow")
  }
}

///|
/// Get the result types from a block type
fn get_block_result_types(
  block_type : @types.BlockType,
  func_types : Array[@types.FuncType],
) -> Array[Type] {
  match block_type {
    Empty => []
    Value(vt) => [type_from_wasm(vt)]
    MultiValue(vts) => vts.map(type_from_wasm)
    InlineType(_, results) => results.map(type_from_wasm)
    TypeIndex(idx) =>
      if idx < func_types.length() {
        func_types[idx].results.map(type_from_wasm)
      } else {
        []
      }
  }
}

///|
/// Get the parameter types from a block type
fn get_block_param_types(
  block_type : @types.BlockType,
  func_types : Array[@types.FuncType],
) -> Array[Type] {
  match block_type {
    Empty => []
    Value(_) => [] // Simple block types have no params
    MultiValue(_) => [] // MultiValue blocks have no params (result-only)
    InlineType(params, _) => params.map(type_from_wasm)
    TypeIndex(idx) =>
      if idx < func_types.length() {
        func_types[idx].params.map(type_from_wasm)
      } else {
        []
      }
  }
}

///|
/// Translate a sequence of WASM instructions
fn Translator::translate(
  self : Translator,
  instrs : Array[@types.Instruction],
) -> Function {
  self.func_env.emit_cancellation_safepoint(self.builder, self.vmctx)
  // Initialize locals with default values in the default entry block.
  //
  // Cranelift alignment:
  // - hoist zero/null initializer construction out of the per-local loop
  // - assign many locals from a shared initializer SSA value
  //
  // This matches Cranelift's `declare_locals` behavior where each local-decl
  // group reuses one `init` value across all locals in that group, instead of
  // materializing a fresh constant instruction per local slot.
  let mut init_i32 : Value? = None
  let mut init_i64 : Value? = None
  let mut init_f32 : Value? = None
  let mut init_f64 : Value? = None
  let mut init_funcref : Value? = None
  let mut init_externref : Value? = None
  let mut init_v128 : Value? = None
  for i, loc in self.locals {
    // Skip parameters - they already have values
    // Use num_wasm_params (not IR params.length which includes vmctx)
    if i >= self.num_wasm_params {
      let init = match loc.ty {
        I32 =>
          if init_i32 is Some(v) {
            v
          } else {
            let v = self.builder.iconst_i32(0)
            init_i32 = Some(v)
            v
          }
        I64 =>
          if init_i64 is Some(v) {
            v
          } else {
            let v = self.builder.iconst_i64(0L)
            init_i64 = Some(v)
            v
          }
        F32 =>
          if init_f32 is Some(v) {
            v
          } else {
            let v = self.builder.fconst_f32(0.0)
            init_f32 = Some(v)
            v
          }
        F64 =>
          if init_f64 is Some(v) {
            v
          } else {
            let v = self.builder.fconst_f64(0.0)
            init_f64 = Some(v)
            v
          }
        Ptr | Ref | CallableRef =>
          if init_funcref is Some(v) {
            v
          } else {
            let v = self.builder.iconst(loc.ty, @wasm_milkir.NULL_REF)
            init_funcref = Some(v)
            v
          }
        OpaqueRef =>
          if init_externref is Some(v) {
            v
          } else {
            let v = self.builder.iconst(OpaqueRef, @wasm_milkir.NULL_REF)
            init_externref = Some(v)
            v
          }
        V128 =>
          if init_v128 is Some(v) {
            v
          } else {
            let v = self.builder.v128_const(Bytes::make(16, b'\x00'))
            init_v128 = Some(v)
            v
          }
      }
      self.locals[i] = init
    }
  }

  // Translate instructions
  // Note: block_stack does NOT include a function-level frame
  // translate_br/translate_br_table handle function-level jumps specially
  for instr in instrs {
    self.translate_instruction(instr)
  }

  // Handle function end
  let block = self.builder.current_block()
  if !self.is_unreachable && block.terminator is None {
    // Normal fall-through: check if return_continuation was created
    if self.return_continuation is Some(ret_cont) {
      // Someone jumped to function level, need to go through continuation
      let args : Array[Value] = []
      for _ in 0.. 0 {
          args.push(self.pop())
        }
      }
      args.rev_in_place()
      self.builder.jump(ret_cont, args)
    } else {
      // No one jumped to function level, just return directly
      let return_vals : Array[Value] = []
      for _ in 0.. 0 {
          return_vals.push(self.pop())
        }
      }
      return_vals.rev_in_place()
      self.builder.return_(return_vals)
    }
  }

  // If return_continuation was created, emit it
  if self.return_continuation is Some(ret_cont) {
    self.builder.switch_to_block(ret_cont)
    let return_vals : Array[Value] = []
    for i in 0.. Block {
  match self.return_continuation {
    Some(block) => block
    None => {
      let ret_cont = self.builder.create_block()
      // Add block parameters for function results.
      //
      // No local parameters: this block only returns the result values, so a
      // local threaded into it could never be read. Every branch to function
      // level therefore passes results and nothing else.
      for ty in self.func_result_types {
        self.builder.add_block_param(ret_cont, ty) |> ignore
      }
      self.return_continuation = Some(ret_cont)
      ret_cont
    }
  }
}

///|
/// Helper for emitting catch branch with exception values
/// Gets exception values for the tag and branches to handler label
fn Translator::emit_catch_branch(
  self : Translator,
  tag_idx : Int,
  label_depth : Int,
  handler_id : Int,
) -> Unit {
  // Get the tag's parameter types
  let tag_types = self.get_tag_param_types(tag_idx)

  // Get exception values BEFORE calling try_table_end (which frees them)
  let args = self.read_exception_payload(tag_types)

  // Pop the exception handler AFTER reading values
  // This is required so that throw_ref doesn't loop back to the same handler
  @wasm_milkir.try_table_end(self.builder, handler_id)

  // Branch to the handler label
  let idx = self.block_stack.length() - 1 - label_depth
  if idx >= 0 && idx < self.block_stack.length() {
    let frame = self.block_stack[idx]
    // Also pass the locals the handler's block carries (for SSA correctness)
    self.push_local_args(args, frame.local_params)
    self.mark_block_frame_reachable(idx)
    self.builder.jump(frame.block, args)
  } else {
    // Function-level branch, which carries no locals
    let ret_cont = self.get_or_create_return_continuation()
    self.builder.jump(ret_cont, args)
  }
}

///|
/// Helper for emitting catch_ref branch (includes exnref on stack)
fn Translator::emit_catch_ref_branch(
  self : Translator,
  tag_idx : Int,
  label_depth : Int,
  handler_id : Int,
) -> Unit {
  // Get the tag's parameter types
  let tag_types = self.get_tag_param_types(tag_idx)

  // Get exception values BEFORE calling try_table_end (which frees them)
  let args = self.read_exception_payload(tag_types)

  // Add exnref (placeholder - use exception tag as simple exnref)
  let exnref = @wasm_milkir.get_exception_tag(self.builder)
  let exnref_i64 = self.builder.sextend(I64, exnref)
  args.push(self.builder.bitcast(Ref, exnref_i64))

  // Pop the exception handler AFTER reading values
  @wasm_milkir.try_table_end(self.builder, handler_id)

  // Branch to the handler label
  let idx = self.block_stack.length() - 1 - label_depth
  if idx >= 0 && idx < self.block_stack.length() {
    let frame = self.block_stack[idx]
    self.push_local_args(args, frame.local_params)
    self.mark_block_frame_reachable(idx)
    self.builder.jump(frame.block, args)
  } else {
    let ret_cont = self.get_or_create_return_continuation()
    self.builder.jump(ret_cont, args)
  }
}

///|
/// Helper for emitting catch_all branch (no exception values)
fn Translator::emit_catch_all_branch(
  self : Translator,
  label_depth : Int,
  handler_id : Int,
) -> Unit {
  // Pop the exception handler before branching to the catch target
  @wasm_milkir.try_table_end(self.builder, handler_id)

  // catch_all doesn't pass exception values, just branches
  let args : Array[Value] = []

  // Branch to the handler label
  let idx = self.block_stack.length() - 1 - label_depth
  if idx >= 0 && idx < self.block_stack.length() {
    let frame = self.block_stack[idx]
    self.push_local_args(args, frame.local_params)
    self.mark_block_frame_reachable(idx)
    self.builder.jump(frame.block, args)
  } else {
    let ret_cont = self.get_or_create_return_continuation()
    self.builder.jump(ret_cont, args)
  }
}

///|
/// Helper for emitting catch_all_ref branch (includes exnref)
fn Translator::emit_catch_all_ref_branch(
  self : Translator,
  label_depth : Int,
  handler_id : Int,
) -> Unit {
  // Pop the exception handler before branching to the catch target
  @wasm_milkir.try_table_end(self.builder, handler_id)
  let args : Array[Value] = []

  // Add exnref (placeholder)
  let exnref = @wasm_milkir.get_exception_tag(self.builder)
  let exnref_i64 = self.builder.sextend(I64, exnref)
  args.push(self.builder.bitcast(Ref, exnref_i64))

  // Branch to the handler label
  let idx = self.block_stack.length() - 1 - label_depth
  if idx >= 0 && idx < self.block_stack.length() {
    let frame = self.block_stack[idx]
    self.push_local_args(args, frame.local_params)
    self.mark_block_frame_reachable(idx)
    self.builder.jump(frame.block, args)
  } else {
    let ret_cont = self.get_or_create_return_continuation()
    self.builder.jump(ret_cont, args)
  }
}

///|
/// Get the parameter types for a tag
fn Translator::get_tag_param_types(
  self : Translator,
  tag_idx : Int,
) -> Array[@types.ValueType] {
  if tag_idx >= 0 && tag_idx < self.tags.length() {
    let tag = self.tags[tag_idx]
    if tag.type_idx >= 0 && tag.type_idx < self.func_types.length() {
      let func_type = self.func_types[tag.type_idx]
      return func_type.params
    }
  }
  [] // Default to empty array if tag not found
}

///|
/// Helper for binary i32 operations
fn Translator::translate_binary_i32(
  self : Translator,
  op : (FunctionBuilder, Value, Value) -> Value,
) -> Unit {
  let b = self.pop()
  let a = self.pop()
  let result = op(self.builder, a, b)
  self.push(result)
}

///|
/// Helper for binary i64 operations
fn Translator::translate_binary_i64(
  self : Translator,
  op : (FunctionBuilder, Value, Value) -> Value,
) -> Unit {
  let b = self.pop()
  let a = self.pop()
  let result = op(self.builder, a, b)
  self.push(result)
}

///|
/// Helper for binary f32 operations
fn Translator::translate_binary_f32(
  self : Translator,
  op : (FunctionBuilder, Value, Value) -> Value,
) -> Unit {
  let b = self.pop()
  let a = self.pop()
  let result = op(self.builder, a, b)
  self.push(result)
}

///|
/// Helper for binary f64 operations
fn Translator::translate_binary_f64(
  self : Translator,
  op : (FunctionBuilder, Value, Value) -> Value,
) -> Unit {
  let b = self.pop()
  let a = self.pop()
  let result = op(self.builder, a, b)
  self.push(result)
}

///|
/// Helper for unary f32 operations
fn Translator::translate_unary_f32(
  self : Translator,
  op : (FunctionBuilder, Value) -> Value,
) -> Unit {
  let a = self.pop()
  let result = op(self.builder, a)
  self.push(result)
}

///|
/// Helper for unary f64 operations
fn Translator::translate_unary_f64(
  self : Translator,
  op : (FunctionBuilder, Value) -> Value,
) -> Unit {
  let a = self.pop()
  let result = op(self.builder, a)
  self.push(result)
}

///|
/// Helper for unary i32 operations
fn Translator::translate_unary_i32(
  self : Translator,
  op : (FunctionBuilder, Value) -> Value,
) -> Unit {
  let a = self.pop()
  let result = op(self.builder, a)
  self.push(result)
}

///|
/// Helper for unary i64 operations
fn Translator::translate_unary_i64(
  self : Translator,
  op : (FunctionBuilder, Value) -> Value,
) -> Unit {
  let a = self.pop()
  let result = op(self.builder, a)
  self.push(result)
}

///|
/// Helper for integer comparisons
fn Translator::translate_icmp(self : Translator, cc : IntCC) -> Unit {
  let b = self.pop()
  let a = self.pop()
  let result = self.builder.icmp(cc, a, b)
  self.push(result)
}

///|
/// Helper for float comparisons
fn Translator::translate_fcmp(self : Translator, cc : FloatCC) -> Unit {
  let b = self.pop()
  let a = self.pop()
  let result = self.builder.fcmp(cc, a, b)
  self.push(result)
}

///|
/// Helper for SIMD binary operations (v128, v128 -> v128)
fn Translator::translate_simd_binary(
  self : Translator,
  opcode : @milkir.VectorOp,
) -> Unit {
  let b = self.pop()
  let a = self.pop()
  let result = self.builder.emit_inst(V128, Vector(opcode), [a, b])
  self.push(result)
}

///|
/// Helper for SIMD unary operations (v128 -> v128)
fn Translator::translate_simd_unary(
  self : Translator,
  opcode : @milkir.VectorOp,
) -> Unit {
  let a = self.pop()
  let result = self.builder.emit_inst(V128, Vector(opcode), [a])
  self.push(result)
}

///|
/// Helper for SIMD to i32 operations (v128 -> i32)
fn Translator::translate_simd_to_i32(
  self : Translator,
  opcode : @milkir.VectorOp,
) -> Unit {
  let a = self.pop()
  let result = self.builder.emit_inst(I32, Vector(opcode), [a])
  self.push(result)
}

///|
/// Helper for SIMD shift operations (v128, i32 -> v128)
fn Translator::translate_simd_shift(
  self : Translator,
  opcode : @milkir.VectorOp,
) -> Unit {
  let shift = self.pop()
  let vec = self.pop()
  let result = self.builder.emit_inst(V128, Vector(opcode), [vec, shift])
  self.push(result)
}

///|
/// Helper for SIMD load operations (addr -> v128)
/// Emits bounds check and load
fn Translator::translate_simd_load(
  self : Translator,
  memidx : Int,
  offset : Int64,
  addr : Value,
  opcode : @milkir.VectorMemoryOp,
) -> Value {
  // Determine access size based on opcode
  let access_size = match opcode {
    LoadExtend(_, _) | LoadSplat(I64) | LoadZero(I64) => 8
    LoadSplat(I32) | LoadZero(I32) => 4
    LoadSplat(I16) => 2
    LoadSplat(I8) => 1
    _ => 16
  }
  // Emit bounds check
  let effective_addr = self.func_env.emit_bounds_check(
    self.builder,
    self.vmctx,
    memidx,
    addr,
    offset,
    access_size,
  )
  // Emit the SIMD load instruction
  self.builder.v128_load_with_addr(opcode, effective_addr)
}

///|
/// Helper for SIMD load lane operations (addr, v128 -> v128)
fn Translator::translate_simd_load_lane(
  self : Translator,
  memidx : Int,
  offset : Int64,
  addr : Value,
  vec : Value,
  opcode : @milkir.VectorMemoryOp,
) -> Value {
  // Emit bounds check
  let lane_size = match opcode {
    LoadLane(I8, _) => 1
    LoadLane(I16, _) => 2
    LoadLane(I32, _) => 4
    LoadLane(I64, _) => 8
    _ => abort("invalid vector lane-load operation")
  }
  let effective_addr = self.func_env.emit_bounds_check(
    self.builder,
    self.vmctx,
    memidx,
    addr,
    offset,
    lane_size,
  )
  // Emit the SIMD load lane instruction
  self.builder.v128_load_lane_with_addr(opcode, effective_addr, vec)
}

///|
/// Helper for SIMD store lane operations (addr, v128 -> void)
fn Translator::translate_simd_store_lane(
  self : Translator,
  memidx : Int,
  offset : Int64,
  addr : Value,
  vec : Value,
  opcode : @milkir.VectorMemoryOp,
) -> Unit {
  // Emit bounds check
  let lane_size = match opcode {
    StoreLane(I8, _) => 1
    StoreLane(I16, _) => 2
    StoreLane(I32, _) => 4
    StoreLane(I64, _) => 8
    _ => abort("invalid vector lane-store operation")
  }
  let effective_addr = self.func_env.emit_bounds_check(
    self.builder,
    self.vmctx,
    memidx,
    addr,
    offset,
    lane_size,
  )
  // Emit the SIMD store lane instruction
  self.builder.v128_store_lane_with_addr(opcode, effective_addr, vec)
}

///|
/// Translate a block construct
///
/// For proper SSA form, the locals the body writes -- nested regions included
/// -- are threaded through the continuation block as parameters, so a value
/// modified inside the block reaches the code after it as a phi. Locals the
/// body never writes hold the same value on every edge into the continuation
/// and get no parameter; `region_local_params` states the invariant.
fn Translator::translate_block(
  self : Translator,
  block_type : @types.BlockType,
  body : Array[@types.Instruction],
) -> Unit {
  // Save the unreachable state from outer context
  let outer_is_unreachable = self.is_unreachable
  let result_types = get_block_result_types(block_type, self.func_types)
  let param_types = get_block_param_types(block_type, self.func_types)
  let continuation = self.builder.create_block()

  // Typed blocks: pop params from the outer stack, then make them available in the body.
  // This mirrors validation semantics (params are not part of the outer stack height).
  let param_values : Array[Value] = []
  if !outer_is_unreachable {
    for _ in 0.. ignore
  }

  // Add block parameters for the locals the body can redefine
  let local_params = self.region_local_params([body])
  let local_param_start = result_types.length()
  self.add_local_block_params(continuation, local_params)

  // Push block frame
  let frame : BlockFrame = {
    block: continuation,
    result_types,
    local_params,
    stack_height: stack_height_after_params,
    has_predecessor: false,
  }
  let frame_idx = self.block_stack.length()
  self.block_stack.push(frame)

  // Reset unreachable for block body (block entry is reachable if outer is)
  // If outer is unreachable, the whole block is dead code
  self.is_unreachable = outer_is_unreachable

  // Push block params onto the value stack (they become available inside the body).
  for v in param_values {
    self.push(v)
  }

  // Translate body
  for instr in body {
    self.translate_instruction(instr)
  }

  // Fall through to continuation (only if not unreachable)
  let block = self.builder.current_block()
  if !self.is_unreachable && block.terminator is None {
    // Collect results from stack
    let args : Array[Value] = []
    for _ in 0.. ignore

  // When unreachable, we need to restore stack to block entry height
  // because unreachable code may have pushed values that shouldn't persist
  while self.value_stack.length() > frame.stack_height {
    self.pop() |> ignore
  }

  // Switch to continuation
  self.builder.switch_to_block(continuation)

  self.is_unreachable = outer_is_unreachable || !continuation_reachable
  if !continuation_reachable {
    self.builder.trap("unreachable block continuation")
  }

  // Push block results onto stack
  if continuation_reachable {
    for i, _ty in result_types {
      self.push(continuation.params[i].0)
    }

    // Update the carried locals to use the continuation's phi values
    self.adopt_local_params(continuation, local_params, local_param_start)
  }
}

///|
/// Translate a loop construct
///
/// For proper SSA form, the locals the body writes are threaded through the
/// loop header as parameters, which is what makes them loop-carried. A local
/// the body never writes has the same value on the entry edge and every back
/// edge, so it needs no parameter; see `region_local_params`.
fn Translator::translate_loop(
  self : Translator,
  block_type : @types.BlockType,
  body : Array[@types.Instruction],
) -> Unit {
  // Save the unreachable state from outer context
  let outer_is_unreachable = self.is_unreachable
  let result_types = get_block_result_types(block_type, self.func_types)
  let param_types = get_block_param_types(block_type, self.func_types)
  let loop_header = self.builder.create_block()
  let continuation = self.builder.create_block()

  // Add loop header parameters for explicit block params
  for ty in param_types {
    self.builder.add_block_param(loop_header, ty) |> ignore
  }

  // Add loop header parameters for the locals the body can redefine, which are
  // exactly the ones a back edge can disagree with the entry edge about
  let local_params = self.region_local_params([body])
  let local_param_start = param_types.length()
  self.add_local_block_params(loop_header, local_params)

  // Add continuation parameters for results
  for ty in result_types {
    self.builder.add_block_param(continuation, ty) |> ignore
  }

  // Jump to loop header with current stack values AND current local values
  // (only if not unreachable)
  if !outer_is_unreachable {
    let header_args : Array[Value] = []
    // First, explicit block params from stack
    for _ in 0.. ignore

  // Fall through to continuation (only if not unreachable)
  let mut loop_falls_through = false
  let block = self.builder.current_block()
  if !self.is_unreachable && block.terminator is None {
    let args : Array[Value] = []
    for _ in 0.. frame.stack_height {
    self.pop() |> ignore
  }

  // Switch to continuation
  self.builder.switch_to_block(continuation)

  self.is_unreachable = outer_is_unreachable || !loop_falls_through
  if !loop_falls_through {
    self.builder.trap("unreachable loop continuation")
  }

  // Push results onto stack
  if loop_falls_through {
    for i, _ty in result_types {
      self.push(continuation.params[i].0)
    }
  }
}