///|
fn type_byte_size(ty : @milkir.Type) -> Int {
  match ty {
    I32 | F32 => 4
    I64 | F64 | Ptr | Ref | CallableRef | OpaqueRef => 8
    V128 => 16
  }
}

///|
fn zero_value(
  builder : @milkir.FunctionBuilder,
  ty : @milkir.Type,
) -> @milkir.Value? {
  match ty {
    I32 => Some(builder.iconst_i32(0))
    I64 | Ref | Ptr | CallableRef | OpaqueRef => Some(builder.iconst_i64(0L))
    F32 => Some(builder.fconst_f32(0.0))
    F64 => Some(builder.fconst_f64(0.0))
    _ => None
  }
}

///|
fn pop_value(stack : Array[@milkir.Value]) -> @milkir.Value? {
  match stack.pop() {
    Some(value) => Some(value)
    None => None
  }
}

///|
fn push_binary(
  stack : Array[@milkir.Value],
  op : (@milkir.Value, @milkir.Value) -> @milkir.Value,
) -> Bool {
  guard pop_value(stack) is Some(rhs) else { return false }
  guard pop_value(stack) is Some(lhs) else { return false }
  stack.push(op(lhs, rhs))
  true
}

///|
fn emit_op(
  builder : @milkir.FunctionBuilder,
  ty : @milkir.Type,
  opcode : @milkir.Opcode,
  operands : Array[@milkir.Value],
) -> @milkir.Value? {
  Some(builder.emit_inst(ty, opcode, operands))
}

///|
fn emit_void_op(
  builder : @milkir.FunctionBuilder,
  opcode : @milkir.Opcode,
  operands : Array[@milkir.Value],
) -> Bool {
  builder.emit_void_inst(opcode, operands)
  true
}

///|
fn emit_multi_op(
  builder : @milkir.FunctionBuilder,
  result_types : Array[@milkir.Type],
  opcode : @milkir.Opcode,
  operands : Array[@milkir.Value],
) -> Array[@milkir.Value]? {
  Some(builder.emit_multi_inst(result_types, opcode, operands))
}

///|
fn push_unary_opcode(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  opcode : @milkir.Opcode,
) -> Bool {
  guard pop_value(stack) is Some(value) else { return false }
  guard emit_op(builder, value.ty, opcode, [value]) is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_unary_opcode_as(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  ty : @milkir.Type,
  opcode : @milkir.Opcode,
) -> Bool {
  guard pop_value(stack) is Some(value) else { return false }
  guard emit_op(builder, ty, opcode, [value]) is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_binary_opcode(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  opcode : @milkir.Opcode,
) -> Bool {
  guard pop_value(stack) is Some(rhs) else { return false }
  guard pop_value(stack) is Some(lhs) else { return false }
  guard emit_op(builder, lhs.ty, opcode, [lhs, rhs]) is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_i32_binary_opcode(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  opcode : @milkir.Opcode,
) -> Bool {
  guard pop_value(stack) is Some(rhs) else { return false }
  guard pop_value(stack) is Some(lhs) else { return false }
  guard emit_op(builder, I32, opcode, [lhs, rhs]) is Some(raw) else {
    return false
  }
  guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [raw]) is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_i32_shift_opcode(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  opcode : @milkir.Opcode,
) -> Bool {
  guard pop_value(stack) is Some(rhs) else { return false }
  guard pop_value(stack) is Some(lhs) else { return false }
  guard emit_op(builder, I32, Scalar(IntBinary(And)), [
      rhs,
      builder.iconst_i32(31),
    ])
    is Some(masked) else {
    return false
  }
  guard emit_op(builder, I32, opcode, [lhs, masked]) is Some(raw) else {
    return false
  }
  guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [raw]) is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_i32_clz(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  guard pop_value(stack) is Some(value) else { return false }
  guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [value])
    is Some(extended) else {
    return false
  }
  guard emit_op(builder, I64, Scalar(IntUnary(CountLeadingZeros)), [extended])
    is Some(raw_count) else {
    return false
  }
  guard emit_op(builder, I64, Scalar(IntBinary(Sub)), [
      raw_count,
      builder.iconst_i64(32L),
    ])
    is Some(i32_count) else {
    return false
  }
  guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [i32_count])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_i32_ctz(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  guard pop_value(stack) is Some(value) else { return false }
  guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [value])
    is Some(extended) else {
    return false
  }
  guard emit_op(builder, I64, Scalar(IntBinary(Or)), [
      extended,
      builder.iconst_i64(0x1_0000_0000L),
    ])
    is Some(nonzero) else {
    return false
  }
  guard emit_op(builder, I64, Scalar(IntUnary(CountTrailingZeros)), [nonzero])
    is Some(raw_count) else {
    return false
  }
  guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [raw_count])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_i32_popcnt(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  guard pop_value(stack) is Some(value) else { return false }
  guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [value])
    is Some(extended) else {
    return false
  }
  guard emit_op(builder, I64, Scalar(IntUnary(PopulationCount)), [extended])
    is Some(raw_count) else {
    return false
  }
  guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [raw_count])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_int_cmp(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  cc : @milkir.IntCC,
) -> Bool {
  guard pop_value(stack) is Some(rhs) else { return false }
  guard pop_value(stack) is Some(lhs) else { return false }
  guard emit_op(builder, I32, Scalar(IntCompare(cc)), [lhs, rhs])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_float_cmp(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  cc : @milkir.FloatCC,
) -> Bool {
  guard pop_value(stack) is Some(rhs) else { return false }
  guard pop_value(stack) is Some(lhs) else { return false }
  guard emit_op(builder, I32, Scalar(FloatCompare(cc)), [lhs, rhs])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_float_copysign(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  float_ty : @milkir.Type,
  int_ty : @milkir.Type,
  magnitude_mask : Int64,
  sign_mask : Int64,
) -> Bool {
  guard pop_value(stack) is Some(sign_source) else { return false }
  guard pop_value(stack) is Some(magnitude_source) else { return false }
  guard emit_op(builder, int_ty, Scalar(Convert(Bitcast)), [magnitude_source])
    is Some(x_bits) else {
    return false
  }
  guard emit_op(builder, int_ty, Scalar(Convert(Bitcast)), [sign_source])
    is Some(y_bits) else {
    return false
  }
  let magnitude_mask_value = builder.iconst(int_ty, magnitude_mask)
  guard emit_op(builder, int_ty, Scalar(IntBinary(And)), [
      x_bits, magnitude_mask_value,
    ])
    is Some(magnitude) else {
    return false
  }
  let sign_mask_value = builder.iconst(int_ty, sign_mask)
  guard emit_op(builder, int_ty, Scalar(IntBinary(And)), [
      y_bits, sign_mask_value,
    ])
    is Some(sign) else {
    return false
  }
  guard emit_op(builder, int_ty, Scalar(IntBinary(Or)), [magnitude, sign])
    is Some(result_bits) else {
    return false
  }
  guard emit_op(builder, float_ty, Scalar(Convert(Bitcast)), [result_bits])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_i31_new(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  guard pop_value(stack) is Some(value) else { return false }
  let masked = builder.iconst_i32(@types.I32_MAX)
  guard emit_op(builder, I32, Scalar(IntBinary(And)), [value, masked])
    is Some(payload) else {
    return false
  }
  guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [payload])
    is Some(payload64) else {
    return false
  }
  let shift = builder.iconst_i64(1L)
  guard emit_op(builder, I64, Scalar(IntBinary(ShiftLeft)), [payload64, shift])
    is Some(shifted) else {
    return false
  }
  let tag = builder.iconst_i64(1L)
  guard emit_op(builder, Ref, Scalar(IntBinary(Or)), [shifted, tag])
    is Some(encoded) else {
    return false
  }
  stack.push(encoded)
  true
}

///|
fn emit_i31_null_check(
  builder : @milkir.FunctionBuilder,
  value : @milkir.Value,
) -> Bool {
  guard emit_op(builder, value.ty, Scalar(IntConst(@wasm_milkir.NULL_REF)), [])
    is Some(null_ref) else {
    return false
  }
  guard emit_op(builder, I32, Scalar(IntCompare(Eq)), [value, null_ref])
    is Some(is_null) else {
    return false
  }
  let trap_block = builder.create_block()
  let continue_block = builder.create_block()
  builder.brnz(is_null, trap_block, continue_block)
  builder.switch_to_block(trap_block)
  builder.trap("null i31 reference")
  builder.switch_to_block(continue_block)
  true
}

///|
fn push_i31_get(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  signed : Bool,
) -> Bool {
  guard pop_value(stack) is Some(value) else { return false }
  if !emit_i31_null_check(builder, value) {
    return false
  }
  guard emit_op(builder, I64, Scalar(Convert(Bitcast)), [value]) is Some(bits) else {
    return false
  }
  let shift = builder.iconst_i64(1L)
  guard emit_op(builder, I64, Scalar(IntBinary(UnsignedShiftRight)), [
      bits, shift,
    ])
    is Some(decoded64) else {
    return false
  }
  guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [decoded64])
    is Some(decoded32) else {
    return false
  }
  if signed {
    let sign_shift = builder.iconst_i32(1)
    guard emit_op(builder, I32, Scalar(IntBinary(ShiftLeft)), [
        decoded32, sign_shift,
      ])
      is Some(left) else {
      return false
    }
    guard emit_op(builder, I32, Scalar(IntBinary(SignedShiftRight)), [
        left, sign_shift,
      ])
      is Some(result) else {
      return false
    }
    stack.push(result)
  } else {
    let mask = builder.iconst_i32(@types.I32_MAX)
    guard emit_op(builder, I32, Scalar(IntBinary(And)), [decoded32, mask])
      is Some(result) else {
      return false
    }
    stack.push(result)
  }
  true
}

///|
fn push_select(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  guard pop_value(stack) is Some(cond) else { return false }
  guard pop_value(stack) is Some(false_value) else { return false }
  guard pop_value(stack) is Some(true_value) else { return false }
  stack.push(builder.select(cond, true_value, false_value))
  true
}

///|
fn get_global_func_type(
  mod_ : @types.Module,
  func_idx : Int,
) -> @types.FuncType? {
  if func_idx < 0 {
    return None
  }
  if func_idx < mod_.imports.length() {
    match mod_.imports[func_idx].desc {
      Func(type_idx) =>
        if type_idx >= 0 && type_idx < mod_.types.length() {
          match mod_.types[type_idx].composite {
            Func(func_type) => Some(func_type)
            _ => None
          }
        } else {
          None
        }
      _ => None
    }
  } else {
    let local_idx = func_idx - mod_.imports.length()
    if local_idx >= 0 && local_idx < mod_.funcs.length() {
      let type_idx = mod_.funcs[local_idx]
      if type_idx >= 0 && type_idx < mod_.types.length() {
        match mod_.types[type_idx].composite {
          Func(func_type) => Some(func_type)
          _ => None
        }
      } else {
        None
      }
    } else {
      None
    }
  }
}

///|
fn collect_global_types(mod_ : @types.Module) -> Array[@types.GlobalType] {
  let globals : Array[@types.GlobalType] = []
  for import_ in mod_.imports {
    if import_.desc is Global(global_type) {
      globals.push(global_type)
    }
  }
  for global in mod_.globals {
    globals.push(global.type_)
  }
  globals
}

///|
fn collect_table_types(mod_ : @types.Module) -> Array[@types.TableType] {
  let tables : Array[@types.TableType] = []
  for import_ in mod_.imports {
    if import_.desc is Table(table_type) {
      tables.push(table_type)
    }
  }
  for table in mod_.tables {
    tables.push(table.type_)
  }
  tables
}

///|
fn collect_memory_types(mod_ : @types.Module) -> Array[@types.MemoryType] {
  let memories : Array[@types.MemoryType] = []
  for import_ in mod_.imports {
    if import_.desc is Memory(memory_type) {
      memories.push(memory_type)
    }
  }
  for memory in mod_.memories {
    memories.push(memory)
  }
  memories
}

///|
fn push_direct_call(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  mod_ : @types.Module,
  vmctx : @milkir.Value?,
  func_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard env.runtime_layout() is Some(layout) else { return false }
  guard get_global_func_type(mod_, func_idx) is Some(func_type) else {
    return false
  }
  let wasm_args : Array[@milkir.Value] = []
  for _ in 0.. Bool {
  guard vmctx is Some(context) else { return false }
  guard env.runtime_layout() is Some(layout) else { return false }
  if global_idx < 0 || global_idx >= global_types.length() {
    return false
  }
  let ty = wasm_value_type_to_milkir(global_types[global_idx].value_type)
  let globals_offset = builder.iconst_i64(layout.globals_offset.to_int64())
  let globals_ptr = builder.load_ptr(I64, context, globals_offset)
  let field_offset = builder.iconst_i64(
    (global_idx * layout.global_value_stride).to_int64(),
  )
  stack.push(builder.load_ptr(ty, globals_ptr, field_offset))
  true
}

///|
fn push_global_set(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  global_types : Array[@types.GlobalType],
  global_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard env.runtime_layout() is Some(layout) else { return false }
  if global_idx < 0 || global_idx >= global_types.length() {
    return false
  }
  guard pop_value(stack) is Some(value) else { return false }
  let ty = wasm_value_type_to_milkir(global_types[global_idx].value_type)
  let globals_offset = builder.iconst_i64(layout.globals_offset.to_int64())
  let globals_ptr = builder.load_ptr(I64, context, globals_offset)
  let field_offset = builder.iconst_i64(
    (global_idx * layout.global_value_stride).to_int64(),
  )
  builder.store_ptr(ty, globals_ptr, value, field_offset)
  true
}

///|
fn table_is_64(table_types : Array[@types.TableType], table_idx : Int) -> Bool? {
  if table_idx < 0 || table_idx >= table_types.length() {
    None
  } else {
    Some(table_types[table_idx].is_table64)
  }
}

///|
fn push_table_size(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  table_types : Array[@types.TableType],
  table_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard env.runtime_layout() is Some(layout) else { return false }
  guard table_is_64(table_types, table_idx) is Some(is_table64) else {
    return false
  }
  let size_i64 = if table_idx == 0 {
    builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.table0_elements_offset.to_int64()),
    )
  } else {
    let sizes_ptr = builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.table_sizes_offset.to_int64()),
    )
    builder.load_ptr(
      I64,
      sizes_ptr,
      builder.iconst_i64((table_idx * layout.pointer_stride).to_int64()),
    )
  }
  if is_table64 {
    stack.push(size_i64)
  } else {
    guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [size_i64])
      is Some(size_i32) else {
      return false
    }
    stack.push(size_i32)
  }
  true
}

///|
fn load_table_size_and_base(
  builder : @milkir.FunctionBuilder,
  env : EmbeddingEnvironment,
  context : @milkir.Value,
  table_idx : Int,
) -> (@milkir.Value, @milkir.Value)? {
  guard env.runtime_layout() is Some(layout) else { return None }
  if table_idx == 0 {
    let size = builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.table0_elements_offset.to_int64()),
    )
    let base = builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.table0_base_offset.to_int64()),
    )
    Some((size, base))
  } else {
    let idx_offset = builder.iconst_i64(
      (table_idx * layout.pointer_stride).to_int64(),
    )
    let sizes_ptr = builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.table_sizes_offset.to_int64()),
    )
    let size = builder.load_ptr(I64, sizes_ptr, idx_offset)
    let tables_ptr = builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.tables_offset.to_int64()),
    )
    let base = builder.load_ptr(I64, tables_ptr, idx_offset)
    Some((size, base))
  }
}

///|
fn push_table_get(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  table_types : Array[@types.TableType],
  table_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard env.runtime_layout() is Some(layout) else { return false }
  guard table_is_64(table_types, table_idx) is Some(is_table64) else {
    return false
  }
  guard pop_value(stack) is Some(elem_idx) else { return false }
  guard load_table_size_and_base(builder, env, context, table_idx)
    is Some((table_size, table_base)) else {
    return false
  }
  let elem_idx_i64 = if is_table64 {
    elem_idx
  } else {
    guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [elem_idx])
      is Some(extended) else {
      return false
    }
    extended
  }
  guard emit_op(builder, I32, Scalar(IntCompare(Ult)), [
      elem_idx_i64, table_size,
    ])
    is Some(in_bounds) else {
    return false
  }
  let trap_block = builder.create_block()
  let continue_block = builder.create_block()
  builder.brnz(in_bounds, continue_block, trap_block)
  builder.switch_to_block(trap_block)
  builder.trap("table out of bounds")
  builder.switch_to_block(continue_block)
  let byte_offset = builder.imul(
    elem_idx_i64,
    builder.iconst_i64(layout.table_entry_stride.to_int64()),
  )
  let addr = builder.iadd(table_base, byte_offset)
  stack.push(
    builder.load_ptr(
      I64,
      addr,
      builder.iconst_i64(layout.table_entry_value_offset.to_int64()),
    ),
  )
  true
}

///|
fn push_table_set(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  table_types : Array[@types.TableType],
  table_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard table_is_64(table_types, table_idx) is Some(_) else { return false }
  guard pop_value(stack) is Some(value) else { return false }
  guard pop_value(stack) is Some(elem_idx) else { return false }
  let one = if elem_idx.ty == I32 {
    builder.iconst_i32(1)
  } else {
    builder.iconst_i64(1L)
  }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(TableFill))
  builder.call_symbol(symbol, None, [
    context,
    builder.iconst_i32(table_idx),
    elem_idx,
    value,
    one,
  ])
  |> ignore
  true
}

///|
fn push_table_fill(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  table_types : Array[@types.TableType],
  table_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard table_is_64(table_types, table_idx) is Some(_) else { return false }
  guard pop3(stack) is Some((dst, value, size)) else { return false }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(TableFill))
  builder.call_symbol(symbol, None, [
    context,
    builder.iconst_i32(table_idx),
    dst,
    value,
    size,
  ])
  |> ignore
  true
}

///|
fn memory_is_64(memory_types : Array[@types.MemoryType], memidx : Int) -> Bool? {
  if memidx < 0 || memidx >= memory_types.length() {
    None
  } else {
    Some(memory_types[memidx].is_memory64)
  }
}

///|
fn memory_index_type(
  memory_types : Array[@types.MemoryType],
  memidx : Int,
) -> @milkir.Type? {
  match memory_is_64(memory_types, memidx) {
    Some(true) => Some(I64)
    Some(false) => Some(I32)
    None => None
  }
}

///|
fn load_memory_base(
  builder : @milkir.FunctionBuilder,
  env : EmbeddingEnvironment,
  context : @milkir.Value,
  memidx : Int,
) -> @milkir.Value? {
  guard env.runtime_layout() is Some(layout) else { return None }
  if memidx == 0 {
    Some(
      builder.load_ptr(
        I64,
        context,
        builder.iconst_i64(layout.memory0_base_offset.to_int64()),
      ),
    )
  } else {
    guard env.memory_descriptor_layout() is Some(memory_layout) else {
      return None
    }
    let memories_ptr = builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.memories_offset.to_int64()),
    )
    let memory_ptr = builder.load_ptr(
      I64,
      memories_ptr,
      builder.iconst_i64((memidx * layout.pointer_stride).to_int64()),
    )
    Some(
      builder.load_ptr(
        I64,
        memory_ptr,
        builder.iconst_i64(memory_layout.base_offset.to_int64()),
      ),
    )
  }
}

///|
fn load_memory_size_bytes(
  builder : @milkir.FunctionBuilder,
  env : EmbeddingEnvironment,
  context : @milkir.Value,
  memidx : Int,
) -> @milkir.Value? {
  guard env.runtime_layout() is Some(layout) else { return None }
  if memidx == 0 {
    Some(
      builder.load_ptr(
        I64,
        context,
        builder.iconst_i64(layout.memory0_size_offset.to_int64()),
      ),
    )
  } else {
    guard env.memory_descriptor_layout() is Some(memory_layout) else {
      return None
    }
    let memories_ptr = builder.load_ptr(
      I64,
      context,
      builder.iconst_i64(layout.memories_offset.to_int64()),
    )
    let memory_ptr = builder.load_ptr(
      I64,
      memories_ptr,
      builder.iconst_i64((memidx * layout.pointer_stride).to_int64()),
    )
    Some(
      builder.load_ptr(
        I64,
        memory_ptr,
        builder.iconst_i64(memory_layout.current_length_offset.to_int64()),
      ),
    )
  }
}

///|
fn push_memory_size(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  memory_types : Array[@types.MemoryType],
  memidx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard memory_index_type(memory_types, memidx) is Some(result_ty) else {
    return false
  }
  guard load_memory_size_bytes(builder, env, context, memidx)
    is Some(size_bytes) else {
    return false
  }
  let page_shift = builder.iconst_i64(
    memory_types[memidx].page_size_log2.to_int64(),
  )
  guard emit_op(builder, I64, Scalar(IntBinary(UnsignedShiftRight)), [
      size_bytes, page_shift,
    ])
    is Some(pages_i64) else {
    return false
  }
  if result_ty == I64 {
    stack.push(pages_i64)
  } else {
    guard emit_op(builder, I32, Scalar(Convert(IntReduce)), [pages_i64])
      is Some(pages_i32) else {
      return false
    }
    stack.push(pages_i32)
  }
  true
}

///|
fn push_memory_grow(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  memory_types : Array[@types.MemoryType],
  memidx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard memory_index_type(memory_types, memidx) is Some(result_ty) else {
    return false
  }
  guard pop_value(stack) is Some(delta) else { return false }
  let delta_i64 = if delta.ty == I64 {
    delta
  } else {
    guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [delta])
      is Some(extended) else {
      return false
    }
    extended
  }
  let max_pages = memory_types[memidx].limits.max
    .map(fn(max) { max.to_int() })
    .unwrap_or(-1)
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(MemoryGrow))
  guard builder.call_symbol(symbol, Some(I32), [
      context,
      builder.iconst_i32(memidx),
      delta_i64,
      builder.iconst_i32(max_pages),
    ])
    is Some(call_result_i32) else {
    return false
  }
  let result = if result_ty == I64 {
    guard emit_op(builder, I64, Scalar(Convert(SignedExtend)), [call_result_i32])
      is Some(extended) else {
      return false
    }
    extended
  } else {
    call_result_i32
  }
  stack.push(result)
  true
}

///|
fn pop3(
  stack : Array[@milkir.Value],
) -> (@milkir.Value, @milkir.Value, @milkir.Value)? {
  guard pop_value(stack) is Some(third) else { return None }
  guard pop_value(stack) is Some(second) else { return None }
  guard pop_value(stack) is Some(first) else { return None }
  Some((first, second, third))
}

///|
fn push_memory_fill(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  memory_types : Array[@types.MemoryType],
  memidx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard memory_index_type(memory_types, memidx) is Some(I32) else {
    // TODO(wasm_frontend): add memory64-capable runtime helper ABI before enabling
    // this reusable call path for memory64 memory.fill.
    return false
  }
  guard pop3(stack) is Some((dst, value, size)) else { return false }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(MemoryFill))
  builder.call_symbol(symbol, None, [
    context,
    builder.iconst_i32(memidx),
    dst,
    value,
    size,
  ])
  |> ignore
  true
}

///|
fn push_memory_copy(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  memory_types : Array[@types.MemoryType],
  dst_memidx : Int,
  src_memidx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard memory_index_type(memory_types, dst_memidx) is Some(I32) else {
    // TODO(wasm_frontend): add memory64-capable runtime helper ABI before enabling
    // this reusable call path for memory64 destination memories.
    return false
  }
  guard memory_index_type(memory_types, src_memidx) is Some(I32) else {
    // TODO(wasm_frontend): add memory64-capable runtime helper ABI before enabling
    // this reusable call path for memory64 source memories.
    return false
  }
  guard pop3(stack) is Some((dst, src, size)) else { return false }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(MemoryCopy))
  builder.call_symbol(symbol, None, [
    context,
    builder.iconst_i32(dst_memidx),
    builder.iconst_i32(src_memidx),
    dst,
    src,
    size,
  ])
  |> ignore
  true
}

///|
fn push_memory_init(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  memory_types : Array[@types.MemoryType],
  memidx : Int,
  data_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard memory_index_type(memory_types, memidx) is Some(I32) else {
    // TODO(wasm_frontend): add memory64-capable runtime helper ABI before enabling
    // this reusable call path for memory64 memory.init.
    return false
  }
  guard pop3(stack) is Some((dst, src, size)) else { return false }
  let dst_i64 = if dst.ty == I64 {
    dst
  } else {
    guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [dst])
      is Some(extended) else {
      return false
    }
    extended
  }
  let src_i64 = if src.ty == I64 {
    src
  } else {
    guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [src])
      is Some(extended) else {
      return false
    }
    extended
  }
  let size_i64 = if size.ty == I64 {
    size
  } else {
    guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [size])
      is Some(extended) else {
      return false
    }
    extended
  }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(MemoryInit))
  builder.call_symbol(symbol, None, [
    context,
    builder.iconst_i32(memidx),
    builder.iconst_i32(data_idx),
    dst_i64,
    src_i64,
    size_i64,
  ])
  |> ignore
  true
}

///|
fn push_data_drop(
  builder : @milkir.FunctionBuilder,
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  data_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(DataDrop))
  builder.call_symbol(symbol, None, [context, builder.iconst_i32(data_idx)])
  |> ignore
  true
}

///|
fn push_table_grow(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  table_types : Array[@types.TableType],
  table_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard table_is_64(table_types, table_idx) is Some(is_table64) else {
    return false
  }
  guard pop_value(stack) is Some(delta) else { return false }
  guard pop_value(stack) is Some(init_value) else { return false }
  let delta_i64 = if delta.ty == I64 {
    delta
  } else {
    guard emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [delta])
      is Some(extended) else {
      return false
    }
    extended
  }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(TableGrow))
  guard builder.call_symbol(symbol, Some(I32), [
      context,
      builder.iconst_i32(table_idx),
      delta_i64,
      init_value,
    ])
    is Some(call_result_i32) else {
    return false
  }
  let result = if is_table64 {
    guard emit_op(builder, I64, Scalar(Convert(SignedExtend)), [call_result_i32])
      is Some(extended) else {
      return false
    }
    extended
  } else {
    call_result_i32
  }
  stack.push(result)
  true
}

///|
fn push_table_copy(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  table_types : Array[@types.TableType],
  dst_table_idx : Int,
  src_table_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard table_is_64(table_types, dst_table_idx) is Some(_) else { return false }
  guard table_is_64(table_types, src_table_idx) is Some(_) else { return false }
  guard pop3(stack) is Some((dst, src, size)) else { return false }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(TableCopy))
  builder.call_symbol(symbol, None, [
    context,
    builder.iconst_i32(dst_table_idx),
    builder.iconst_i32(src_table_idx),
    dst,
    src,
    size,
  ])
  |> ignore
  true
}

///|
fn push_table_init(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  table_types : Array[@types.TableType],
  table_idx : Int,
  elem_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard table_is_64(table_types, table_idx) is Some(_) else { return false }
  guard pop3(stack) is Some((dst, src, size)) else { return false }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(TableInit))
  builder.call_symbol(symbol, None, [
    context,
    builder.iconst_i32(table_idx),
    builder.iconst_i32(elem_idx),
    dst,
    src,
    size,
  ])
  |> ignore
  true
}

///|
fn push_elem_drop(
  builder : @milkir.FunctionBuilder,
  runtime_symbols : @wasm_milkir.RuntimeSymbols,
  vmctx : @milkir.Value?,
  elem_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  let symbol = builder
    .get_function()
    .declare_external_symbol(runtime_symbols.symbol_name(ElemDrop))
  builder.call_symbol(symbol, None, [context, builder.iconst_i32(elem_idx)])
  |> ignore
  true
}

///|
fn push_ref_null(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  ref_type : @types.ValueType,
) -> Bool {
  let ty = wasm_value_type_to_milkir(ref_type)
  guard emit_op(builder, ty, Scalar(IntConst(@wasm_milkir.NULL_REF)), [])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_ref_is_null(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  guard pop_value(stack) is Some(ref_value) else { return false }
  guard emit_op(
      builder,
      ref_value.ty,
      Scalar(IntConst(@wasm_milkir.NULL_REF)),
      [],
    )
    is Some(null_sentinel) else {
    return false
  }
  stack.push(builder.icmp_eq(ref_value, null_sentinel))
  true
}

///|
fn push_ref_func(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  func_idx : Int,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  guard env.runtime_layout() is Some(layout) else { return false }
  let func_table = builder.load_ptr(
    I64,
    context,
    builder.iconst_i64(layout.func_table_offset.to_int64()),
  )
  let raw_func_ptr = builder.load_ptr(
    Ref,
    func_table,
    builder.iconst_i64((func_idx * layout.func_table_entry_stride).to_int64()),
  )
  let tag = builder.iconst(Ref, @wasm_milkir.FUNCREF_TAG)
  guard emit_op(builder, Ref, Scalar(IntBinary(Or)), [raw_func_ptr, tag])
    is Some(result) else {
    return false
  }
  stack.push(result)
  true
}

///|
fn push_ref_as_non_null(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  guard stack.last() is Some(ref_value) else { return false }
  guard emit_op(
      builder,
      ref_value.ty,
      Scalar(IntConst(@wasm_milkir.NULL_REF)),
      [],
    )
    is Some(null_sentinel) else {
    return false
  }
  let is_null = builder.icmp_eq(ref_value, null_sentinel)
  let trap_block = builder.create_block()
  let continue_block = builder.create_block()
  builder.brnz(is_null, trap_block, continue_block)
  builder.switch_to_block(trap_block)
  builder.trap("null reference")
  builder.switch_to_block(continue_block)
  true
}

///|
fn push_ref_eq(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
) -> Bool {
  if !push_int_cmp(builder, stack, Eq) {
    return false
  }
  true
}

///|
fn single_block_result_type(block_type : @types.BlockType) -> @types.ValueType? {
  match block_type {
    Value(ty) => Some(ty)
    _ => None
  }
}

///|
fn push_simple_expr_value(
  builder : @milkir.FunctionBuilder,
  locals : Array[@milkir.Value],
  instr : @types.Instruction,
) -> @milkir.Value? {
  match instr {
    LocalGet(idx) =>
      if idx >= 0 && idx < locals.length() {
        Some(locals[idx])
      } else {
        None
      }
    I32Const(value) => Some(builder.iconst_i32(value))
    I64Const(value) => Some(builder.iconst_i64(value))
    F32Const(value) => Some(builder.fconst_f32(value))
    F64Const(value) => Some(builder.fconst_f64(value))
    RefNull(ref_type) => {
      let ty = wasm_value_type_to_milkir(ref_type)
      emit_op(builder, ty, Scalar(IntConst(@wasm_milkir.NULL_REF)), [])
    }
    _ => None
  }
}

///|
fn push_simple_expr_instr(
  builder : @milkir.FunctionBuilder,
  locals : Array[@milkir.Value],
  expr_stack : Array[@milkir.Value],
  instr : @types.Instruction,
) -> Bool {
  match instr {
    LocalGet(idx) =>
      if idx >= 0 && idx < locals.length() {
        expr_stack.push(locals[idx])
        true
      } else {
        false
      }
    I32Const(value) => {
      expr_stack.push(builder.iconst_i32(value))
      true
    }
    I64Const(value) => {
      expr_stack.push(builder.iconst_i64(value))
      true
    }
    F32Const(value) => {
      expr_stack.push(builder.fconst_f32(value))
      true
    }
    F64Const(value) => {
      expr_stack.push(builder.fconst_f64(value))
      true
    }
    RefNull(ref_type) => {
      guard push_simple_expr_value(builder, locals, RefNull(ref_type))
        is Some(value) else {
        return false
      }
      expr_stack.push(value)
      true
    }
    I32Add =>
      push_i32_binary_opcode(builder, expr_stack, Scalar(IntBinary(Add)))
    I64Add => push_binary(expr_stack, fn(lhs, rhs) { builder.iadd(lhs, rhs) })
    I32Sub =>
      push_i32_binary_opcode(builder, expr_stack, Scalar(IntBinary(Sub)))
    I64Sub => push_binary(expr_stack, fn(lhs, rhs) { builder.isub(lhs, rhs) })
    I32Mul =>
      push_i32_binary_opcode(builder, expr_stack, Scalar(IntBinary(Mul)))
    I64Mul => push_binary(expr_stack, fn(lhs, rhs) { builder.imul(lhs, rhs) })
    I64MulWideU =>
      push_binary_opcode(
        builder,
        expr_stack,
        Scalar(IntBinary(UnsignedMulHigh)),
      )
    I64MulWideS =>
      push_binary_opcode(builder, expr_stack, Scalar(IntBinary(SignedMulHigh)))
    I32And =>
      push_i32_binary_opcode(builder, expr_stack, Scalar(IntBinary(And)))
    I64And => push_binary_opcode(builder, expr_stack, Scalar(IntBinary(And)))
    I32Or => push_i32_binary_opcode(builder, expr_stack, Scalar(IntBinary(Or)))
    I64Or => push_binary_opcode(builder, expr_stack, Scalar(IntBinary(Or)))
    I32Xor =>
      push_i32_binary_opcode(builder, expr_stack, Scalar(IntBinary(Xor)))
    I64Xor => push_binary_opcode(builder, expr_stack, Scalar(IntBinary(Xor)))
    I32Shl =>
      push_i32_shift_opcode(builder, expr_stack, Scalar(IntBinary(ShiftLeft)))
    I64Shl =>
      push_binary_opcode(builder, expr_stack, Scalar(IntBinary(ShiftLeft)))
    I32ShrS =>
      push_i32_shift_opcode(
        builder,
        expr_stack,
        Scalar(IntBinary(SignedShiftRight)),
      )
    I64ShrS =>
      push_binary_opcode(
        builder,
        expr_stack,
        Scalar(IntBinary(SignedShiftRight)),
      )
    I32ShrU =>
      push_i32_shift_opcode(
        builder,
        expr_stack,
        Scalar(IntBinary(UnsignedShiftRight)),
      )
    I64ShrU =>
      push_binary_opcode(
        builder,
        expr_stack,
        Scalar(IntBinary(UnsignedShiftRight)),
      )
    I32Rotl =>
      push_i32_shift_opcode(builder, expr_stack, Scalar(IntBinary(RotateLeft)))
    I64Rotl =>
      push_binary_opcode(builder, expr_stack, Scalar(IntBinary(RotateLeft)))
    I32Rotr =>
      push_i32_shift_opcode(builder, expr_stack, Scalar(IntBinary(RotateRight)))
    I64Rotr =>
      push_binary_opcode(builder, expr_stack, Scalar(IntBinary(RotateRight)))
    I32Clz => push_i32_clz(builder, expr_stack)
    I64Clz =>
      push_unary_opcode(
        builder,
        expr_stack,
        Scalar(IntUnary(CountLeadingZeros)),
      )
    I32Ctz => push_i32_ctz(builder, expr_stack)
    I64Ctz =>
      push_unary_opcode(
        builder,
        expr_stack,
        Scalar(IntUnary(CountTrailingZeros)),
      )
    I32Popcnt => push_i32_popcnt(builder, expr_stack)
    I64Popcnt =>
      push_unary_opcode(builder, expr_stack, Scalar(IntUnary(PopulationCount)))
    I32Eqz => {
      guard pop_value(expr_stack) is Some(value) else { return false }
      expr_stack.push(builder.icmp_eq(value, builder.iconst_i32(0)))
      true
    }
    I64Eqz => {
      guard pop_value(expr_stack) is Some(value) else { return false }
      expr_stack.push(builder.icmp_eq(value, builder.iconst_i64(0L)))
      true
    }
    I32Eq | I64Eq => push_int_cmp(builder, expr_stack, Eq)
    I32Ne | I64Ne => push_int_cmp(builder, expr_stack, Ne)
    I32LtS | I64LtS => push_int_cmp(builder, expr_stack, Slt)
    I32LtU | I64LtU => push_int_cmp(builder, expr_stack, Ult)
    I32GtS | I64GtS => push_int_cmp(builder, expr_stack, Sgt)
    I32GtU | I64GtU => push_int_cmp(builder, expr_stack, Ugt)
    I32LeS | I64LeS => push_int_cmp(builder, expr_stack, Sle)
    I32LeU | I64LeU => push_int_cmp(builder, expr_stack, Ule)
    I32GeS | I64GeS => push_int_cmp(builder, expr_stack, Sge)
    I32GeU | I64GeU => push_int_cmp(builder, expr_stack, Uge)
    F32Add | F64Add =>
      push_binary(expr_stack, fn(lhs, rhs) { builder.fadd(lhs, rhs) })
    F32Sub | F64Sub =>
      push_binary(expr_stack, fn(lhs, rhs) { builder.fsub(lhs, rhs) })
    F32Mul | F64Mul =>
      push_binary(expr_stack, fn(lhs, rhs) { builder.fmul(lhs, rhs) })
    F32Div | F64Div =>
      push_binary_opcode(builder, expr_stack, Scalar(FloatBinary(Div)))
    F32Min | F64Min =>
      push_binary_opcode(builder, expr_stack, Scalar(FloatBinary(Min)))
    F32Max | F64Max =>
      push_binary_opcode(builder, expr_stack, Scalar(FloatBinary(Max)))
    F32Copysign =>
      push_float_copysign(
        builder,
        expr_stack,
        F32,
        I32,
        0x7FFFFFFFL,
        0x80000000U.reinterpret_as_int().to_int64(),
      )
    F64Copysign =>
      push_float_copysign(
        builder,
        expr_stack,
        F64,
        I64,
        0x7FFFFFFFFFFFFFFFL,
        0x8000000000000000UL.reinterpret_as_int64(),
      )
    F32Neg | F64Neg =>
      push_unary_opcode(builder, expr_stack, Scalar(FloatUnary(Neg)))
    F32Abs | F64Abs =>
      push_unary_opcode(builder, expr_stack, Scalar(FloatUnary(Abs)))
    F32Sqrt | F64Sqrt =>
      push_unary_opcode(builder, expr_stack, Scalar(FloatUnary(Sqrt)))
    F32Ceil | F64Ceil =>
      push_unary_opcode(builder, expr_stack, Scalar(FloatUnary(Ceil)))
    F32Floor | F64Floor =>
      push_unary_opcode(builder, expr_stack, Scalar(FloatUnary(Floor)))
    F32Trunc | F64Trunc =>
      push_unary_opcode(builder, expr_stack, Scalar(FloatUnary(Trunc)))
    F32Nearest | F64Nearest =>
      push_unary_opcode(builder, expr_stack, Scalar(FloatUnary(Nearest)))
    F32Eq | F64Eq => push_float_cmp(builder, expr_stack, Eq)
    F32Ne | F64Ne => push_float_cmp(builder, expr_stack, Ne)
    F32Lt | F64Lt => push_float_cmp(builder, expr_stack, Lt)
    F32Gt | F64Gt => push_float_cmp(builder, expr_stack, Gt)
    F32Le | F64Le => push_float_cmp(builder, expr_stack, Le)
    F32Ge | F64Ge => push_float_cmp(builder, expr_stack, Ge)
    I32WrapI64 =>
      push_unary_opcode_as(builder, expr_stack, I32, Scalar(Convert(IntReduce)))
    I64ExtendI32S =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        I64,
        Scalar(Convert(SignedExtend)),
      )
    I64ExtendI32U =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        I64,
        Scalar(Convert(UnsignedExtend)),
      )
    I32Extend8S =>
      push_unary_opcode_as(builder, expr_stack, I32, Scalar(SignExtendFrom(8)))
    I32Extend16S =>
      push_unary_opcode_as(builder, expr_stack, I32, Scalar(SignExtendFrom(16)))
    I64Extend8S =>
      push_unary_opcode_as(builder, expr_stack, I64, Scalar(SignExtendFrom(8)))
    I64Extend16S =>
      push_unary_opcode_as(builder, expr_stack, I64, Scalar(SignExtendFrom(16)))
    I64Extend32S =>
      push_unary_opcode_as(builder, expr_stack, I64, Scalar(SignExtendFrom(32)))
    F32DemoteF64 =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        F32,
        Scalar(Convert(FloatDemote)),
      )
    F64PromoteF32 =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        F64,
        Scalar(Convert(FloatPromote)),
      )
    I32TruncSatF32S | I32TruncSatF64S =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        I32,
        Scalar(Convert(FloatToSignedIntSaturating)),
      )
    I32TruncSatF32U | I32TruncSatF64U =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        I32,
        Scalar(Convert(FloatToUnsignedIntSaturating)),
      )
    I64TruncSatF32S | I64TruncSatF64S =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        I64,
        Scalar(Convert(FloatToSignedIntSaturating)),
      )
    I64TruncSatF32U | I64TruncSatF64U =>
      push_unary_opcode_as(
        builder,
        expr_stack,
        I64,
        Scalar(Convert(FloatToUnsignedIntSaturating)),
      )
    I32ReinterpretF32 =>
      push_unary_opcode_as(builder, expr_stack, I32, Scalar(Convert(Bitcast)))
    I64ReinterpretF64 =>
      push_unary_opcode_as(builder, expr_stack, I64, Scalar(Convert(Bitcast)))
    F32ReinterpretI32 =>
      push_unary_opcode_as(builder, expr_stack, F32, Scalar(Convert(Bitcast)))
    F64ReinterpretI64 =>
      push_unary_opcode_as(builder, expr_stack, F64, Scalar(Convert(Bitcast)))
    RefI31 => push_i31_new(builder, expr_stack)
    AnyConvertExtern | ExternConvertAny =>
      push_unary_opcode(builder, expr_stack, Scalar(Copy))
    Select | SelectTyped(_) => push_select(builder, expr_stack)
    Drop => {
      guard pop_value(expr_stack) is Some(_) else { return false }
      true
    }
    _ => false
  }
}

///|
fn push_simple_expr_body_value(
  builder : @milkir.FunctionBuilder,
  locals : Array[@milkir.Value],
  body : Array[@types.Instruction],
  expected_ty : @milkir.Type,
) -> @milkir.Value? {
  let expr_stack : Array[@milkir.Value] = []
  for instr in body {
    if !push_simple_expr_instr(builder, locals, expr_stack, instr) {
      return None
    }
  }
  if expr_stack.length() != 1 || expr_stack[0].ty != expected_ty {
    None
  } else {
    Some(expr_stack[0])
  }
}

///|
fn push_simple_if_select(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  locals : Array[@milkir.Value],
  block_type : @types.BlockType,
  then_body : Array[@types.Instruction],
  else_body : Array[@types.Instruction],
) -> Bool {
  guard single_block_result_type(block_type) is Some(result_ty) else {
    return false
  }
  guard pop_value(stack) is Some(cond) else { return false }
  let expected_ty = wasm_value_type_to_milkir(result_ty)
  guard push_simple_expr_body_value(builder, locals, then_body, expected_ty)
    is Some(true_value) else {
    return false
  }
  guard push_simple_expr_body_value(builder, locals, else_body, expected_ty)
    is Some(false_value) else {
    return false
  }
  // TODO(wasm_frontend): lower general structured `if` control flow with block
  // parameters and branch labels. This select form is only for pure expressions.
  stack.push(builder.select(cond, true_value, false_value))
  true
}

///|
fn push_memory_base(
  builder : @milkir.FunctionBuilder,
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value,
  memidx : Int,
) -> @milkir.Value? {
  load_memory_base(builder, env, vmctx, memidx)
}

///|
fn extend_memory_addr(
  builder : @milkir.FunctionBuilder,
  memory_types : Array[@types.MemoryType],
  memidx : Int,
  addr : @milkir.Value,
) -> @milkir.Value? {
  if memory_types[memidx].is_memory64 || addr.ty == I64 {
    Some(addr)
  } else {
    emit_op(builder, I64, Scalar(Convert(UnsignedExtend)), [addr])
  }
}

///|
fn push_memory_bounds_check(
  builder : @milkir.FunctionBuilder,
  memory_types : Array[@types.MemoryType],
  memidx : Int,
  addr_i64 : @milkir.Value,
  addr_plus_offset : @milkir.Value,
  memory_size : @milkir.Value,
  access_size : Int,
) -> Bool {
  let size_value = builder.iconst_i64(access_size.to_int64())
  let end_addr = builder.iadd(addr_plus_offset, size_value)
  let trap_block = builder.create_block()
  let continue_block = builder.create_block()
  if memory_types[memidx].is_memory64 {
    guard emit_op(builder, I32, Scalar(IntCompare(Uge)), [
        addr_plus_offset, addr_i64,
      ])
      is Some(no_overflow1) else {
      return false
    }
    let check2_block = builder.create_block()
    builder.brnz(no_overflow1, check2_block, trap_block)
    builder.switch_to_block(check2_block)
    guard emit_op(builder, I32, Scalar(IntCompare(Uge)), [
        end_addr, addr_plus_offset,
      ])
      is Some(no_overflow2) else {
      return false
    }
    let range_block = builder.create_block()
    builder.brnz(no_overflow2, range_block, trap_block)
    builder.switch_to_block(range_block)
  }
  guard emit_op(builder, I32, Scalar(IntCompare(Ule)), [end_addr, memory_size])
    is Some(in_bounds) else {
    return false
  }
  builder.brnz(in_bounds, continue_block, trap_block)
  builder.switch_to_block(trap_block)
  builder.trap("memory out of bounds")
  builder.switch_to_block(continue_block)
  true
}

///|
fn memory_effective_addr(
  builder : @milkir.FunctionBuilder,
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value,
  memory_types : Array[@types.MemoryType],
  memidx : Int,
  wasm_addr : @milkir.Value,
  offset : Int64,
  access_size : Int,
) -> @milkir.Value? {
  guard push_memory_base(builder, env, vmctx, memidx) is Some(base) else {
    return None
  }
  guard extend_memory_addr(builder, memory_types, memidx, wasm_addr)
    is Some(addr_i64) else {
    return None
  }
  let offset_value = builder.iconst_i64(offset)
  let addr_plus_offset = builder.iadd(addr_i64, offset_value)
  if memidx != 0 || memory_types[memidx].is_memory64 {
    guard load_memory_size_bytes(builder, env, vmctx, memidx)
      is Some(memory_size) else {
      return None
    }
    if !push_memory_bounds_check(
        builder, memory_types, memidx, addr_i64, addr_plus_offset, memory_size, access_size,
      ) {
      return None
    }
  }
  Some(builder.iadd(base, addr_plus_offset))
}

///|
fn push_memory_load(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  mod_ : @types.Module,
  memidx : Int,
  ty : @milkir.Type,
  offset : Int64,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  if memidx < 0 || memidx >= mod_.memories.length() {
    return false
  }
  guard pop_value(stack) is Some(wasm_addr) else { return false }
  guard memory_effective_addr(
      builder,
      env,
      context,
      mod_.memories,
      memidx,
      wasm_addr,
      offset,
      type_byte_size(ty),
    )
    is Some(effective_addr) else {
    return false
  }
  stack.push(builder.load_ptr(ty, effective_addr, builder.iconst_i64(0L)))
  true
}

///|
fn push_memory_load_narrow(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  mod_ : @types.Module,
  memidx : Int,
  result_ty : @milkir.Type,
  bits : Int,
  signed : Bool,
  offset : Int64,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  if memidx < 0 || memidx >= mod_.memories.length() {
    return false
  }
  guard pop_value(stack) is Some(wasm_addr) else { return false }
  guard memory_effective_addr(
      builder,
      env,
      context,
      mod_.memories,
      memidx,
      wasm_addr,
      offset,
      bits / 8,
    )
    is Some(effective_addr) else {
    return false
  }
  stack.push(
    builder.load_ptr_narrow(
      result_ty,
      bits,
      signed,
      effective_addr,
      builder.iconst_i64(0L),
    ),
  )
  true
}

///|
fn push_memory_store(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  mod_ : @types.Module,
  memidx : Int,
  ty : @milkir.Type,
  offset : Int64,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  if memidx < 0 || memidx >= mod_.memories.length() {
    return false
  }
  guard pop_value(stack) is Some(value) else { return false }
  guard pop_value(stack) is Some(wasm_addr) else { return false }
  guard memory_effective_addr(
      builder,
      env,
      context,
      mod_.memories,
      memidx,
      wasm_addr,
      offset,
      type_byte_size(ty),
    )
    is Some(effective_addr) else {
    return false
  }
  builder.store_ptr(ty, effective_addr, value, builder.iconst_i64(0L))
  true
}

///|
fn push_memory_store_narrow(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  env : EmbeddingEnvironment,
  vmctx : @milkir.Value?,
  mod_ : @types.Module,
  memidx : Int,
  bits : Int,
  offset : Int64,
) -> Bool {
  guard vmctx is Some(context) else { return false }
  if memidx < 0 || memidx >= mod_.memories.length() {
    return false
  }
  guard pop_value(stack) is Some(value) else { return false }
  guard pop_value(stack) is Some(wasm_addr) else { return false }
  guard memory_effective_addr(
      builder,
      env,
      context,
      mod_.memories,
      memidx,
      wasm_addr,
      offset,
      bits / 8,
    )
    is Some(effective_addr) else {
    return false
  }
  builder.store_ptr_narrow(bits, effective_addr, value, builder.iconst_i64(0L))
  true
}

///|
fn finish_linear_return(
  builder : @milkir.FunctionBuilder,
  stack : Array[@milkir.Value],
  result_count : Int,
) -> Bool {
  if stack.length() < result_count {
    return false
  }
  let popped : Array[@milkir.Value] = []
  for _ in 0.. Bool {
  for instr in body {
    match instr {
      Block(_, nested) =>
        if !append_linear_exit_block_body(out, nested) {
          return false
        }
      Loop(_, nested) =>
        if !append_linear_block_body(out, nested) {
          return false
        }
      Br(_)
      | BrIf(_)
      | BrTable(_, _)
      | BrOnNull(_)
      | BrOnNonNull(_)
      | TryTable(_, _, _) =>
        // TODO(wasm_frontend): lower structured labels and branch stack effects
        // directly instead of relying on the full structured translator path.
        return false
      _ => out.push(instr)
    }
  }
  true
}

///|
fn append_linear_exit_block_body(
  out : Array[@types.Instruction],
  body : Array[@types.Instruction],
) -> Bool {
  for i, instr in body {
    match instr {
      Br(depth) =>
        // TODO(wasm_frontend): model full label-stack branch effects. The native
        // frontend can safely flatten only a terminal direct branch to the
        // current block label. Non-terminal branches may leave expression
        // operands that are unreachable in Wasm semantics but still visible to
        // this linear stack model.
        return depth == 0 && i == body.length() - 1
      BrIf(depth) =>
        if depth == 0 && i == body.length() - 1 {
          // At the end of a block, both taken and not-taken paths leave the
          // same label values on the stack. The condition is still consumed.
          out.push(Drop)
          return true
        } else {
          return false
        }
      BrTable(labels, default_label) =>
        if default_label == 0 && labels.all(label => label == 0) {
          // Terminal table branches to the current block all have the same
          // observable effect: consume the selector and leave current label
          // values for the block result.
          out.push(Drop)
          return true
        } else {
          return false
        }
      BrOnNull(depth) | BrOnNonNull(depth) =>
        if depth == 0 && i == body.length() - 1 {
          // At the end of a block, both reference-branch outcomes leave the
          // same reference value as the current block result in the native
          // frontend's uniform reference representation.
          return true
        } else {
          return false
        }
      _ => if !append_linear_block_body(out, [instr]) { return false }
    }
  }
  true
}

///|
fn body_is_nop_only(body : Array[@types.Instruction]) -> Bool {
  body.all(instr => instr is Nop)
}

///|
fn flatten_linear_blocks(
  body : Array[@types.Instruction],
) -> Array[@types.Instruction]? {
  let out : Array[@types.Instruction] = []
  if append_linear_block_body(out, body) {
    Some(out)
  } else {
    None
  }
}

///|
fn try_lower_linear_function(
  env : EmbeddingEnvironment,
  mod_ : @types.Module,
  func_local_idx : Int,
) -> @milkir.Function? raise LowerError {
  let func_type = get_func_type(mod_, func_local_idx)
  let code = get_code(mod_, func_local_idx)
  guard flatten_linear_blocks(code.body) is Some(body) else { return None }
  let global_types = collect_global_types(mod_)
  let table_types = collect_table_types(mod_)
  let memory_types = collect_memory_types(mod_)
  let runtime_symbols = env.wasm_runtime_symbols()
  let builder = @milkir.FunctionBuilder::FunctionBuilder(
    function_name(mod_, func_local_idx),
  )
  let locals : Array[@milkir.Value] = []
  let mut vmctx : @milkir.Value? = None
  if env.hidden_context_param() is Some(hidden_context_type) {
    vmctx = Some(builder.add_param(hidden_context_type))
  }
  for param in func_type.params {
    locals.push(builder.add_param(wasm_value_type_to_milkir(param)))
  }
  for result in func_type.results {
    builder.add_result(wasm_value_type_to_milkir(result))
  }
  for local_ty in code.locals {
    let milk_ty = wasm_value_type_to_milkir(local_ty)
    guard zero_value(builder, milk_ty) is Some(value) else { return None }
    locals.push(value)
  }
  let stack : Array[@milkir.Value] = []
  let mut terminated = false
  for instr in body {
    if terminated {
      continue
    }
    match instr {
      Nop => ()
      If(block_type, then_body, else_body) =>
        if block_type is Empty &&
          body_is_nop_only(then_body) &&
          body_is_nop_only(else_body) {
          guard pop_value(stack) is Some(_) else { return None }
        } else if !push_simple_if_select(
            builder, stack, locals, block_type, then_body, else_body,
          ) {
          return None
        }
      LocalGet(idx) => {
        if idx < 0 || idx >= locals.length() {
          return None
        }
        stack.push(locals[idx])
      }
      LocalSet(idx) => {
        if idx < 0 || idx >= locals.length() {
          return None
        }
        guard pop_value(stack) is Some(value) else { return None }
        locals[idx] = value
      }
      LocalTee(idx) => {
        if idx < 0 || idx >= locals.length() {
          return None
        }
        guard stack.last() is Some(value) else { return None }
        locals[idx] = value
      }
      GlobalGet(idx) =>
        if !push_global_get(builder, stack, env, vmctx, global_types, idx) {
          return None
        }
      GlobalSet(idx) =>
        if !push_global_set(builder, stack, env, vmctx, global_types, idx) {
          return None
        }
      TableSize(idx) =>
        if !push_table_size(builder, stack, env, vmctx, table_types, idx) {
          return None
        }
      TableGet(idx) =>
        if !push_table_get(builder, stack, env, vmctx, table_types, idx) {
          return None
        }
      TableSet(idx) =>
        if !push_table_set(
            builder, stack, runtime_symbols, vmctx, table_types, idx,
          ) {
          return None
        }
      I32Const(value) => stack.push(builder.iconst_i32(value))
      I64Const(value) => stack.push(builder.iconst_i64(value))
      F32Const(value) => stack.push(builder.fconst_f32(value))
      F64Const(value) => stack.push(builder.fconst_f64(value))
      I32Add =>
        if !push_i32_binary_opcode(builder, stack, Scalar(IntBinary(Add))) {
          return None
        }
      I64Add =>
        if !push_binary(stack, fn(lhs, rhs) { builder.iadd(lhs, rhs) }) {
          return None
        }
      I32Sub =>
        if !push_i32_binary_opcode(builder, stack, Scalar(IntBinary(Sub))) {
          return None
        }
      I64Sub =>
        if !push_binary(stack, fn(lhs, rhs) { builder.isub(lhs, rhs) }) {
          return None
        }
      I32Mul =>
        if !push_i32_binary_opcode(builder, stack, Scalar(IntBinary(Mul))) {
          return None
        }
      I64Mul =>
        if !push_binary(stack, fn(lhs, rhs) { builder.imul(lhs, rhs) }) {
          return None
        }
      I64MulWideU =>
        if !push_binary_opcode(
            builder,
            stack,
            Scalar(IntBinary(UnsignedMulHigh)),
          ) {
          return None
        }
      I64MulWideS =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(SignedMulHigh))) {
          return None
        }
      I32DivS | I64DivS =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(SignedDiv))) {
          return None
        }
      I32DivU | I64DivU =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(UnsignedDiv))) {
          return None
        }
      I32RemS | I64RemS =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(SignedRem))) {
          return None
        }
      I32RemU | I64RemU =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(UnsignedRem))) {
          return None
        }
      I32And =>
        if !push_i32_binary_opcode(builder, stack, Scalar(IntBinary(And))) {
          return None
        }
      I64And =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(And))) {
          return None
        }
      I32Or =>
        if !push_i32_binary_opcode(builder, stack, Scalar(IntBinary(Or))) {
          return None
        }
      I64Or =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(Or))) {
          return None
        }
      I32Xor =>
        if !push_i32_binary_opcode(builder, stack, Scalar(IntBinary(Xor))) {
          return None
        }
      I64Xor =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(Xor))) {
          return None
        }
      I32Shl =>
        if !push_i32_shift_opcode(builder, stack, Scalar(IntBinary(ShiftLeft))) {
          return None
        }
      I64Shl =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(ShiftLeft))) {
          return None
        }
      I32ShrS =>
        if !push_i32_shift_opcode(
            builder,
            stack,
            Scalar(IntBinary(SignedShiftRight)),
          ) {
          return None
        }
      I64ShrS =>
        if !push_binary_opcode(
            builder,
            stack,
            Scalar(IntBinary(SignedShiftRight)),
          ) {
          return None
        }
      I32ShrU =>
        if !push_i32_shift_opcode(
            builder,
            stack,
            Scalar(IntBinary(UnsignedShiftRight)),
          ) {
          return None
        }
      I64ShrU =>
        if !push_binary_opcode(
            builder,
            stack,
            Scalar(IntBinary(UnsignedShiftRight)),
          ) {
          return None
        }
      I32Rotl =>
        if !push_i32_shift_opcode(builder, stack, Scalar(IntBinary(RotateLeft))) {
          return None
        }
      I64Rotl =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(RotateLeft))) {
          return None
        }
      I32Rotr =>
        if !push_i32_shift_opcode(
            builder,
            stack,
            Scalar(IntBinary(RotateRight)),
          ) {
          return None
        }
      I64Rotr =>
        if !push_binary_opcode(builder, stack, Scalar(IntBinary(RotateRight))) {
          return None
        }
      I32Clz => if !push_i32_clz(builder, stack) { return None }
      I64Clz =>
        if !push_unary_opcode(
            builder,
            stack,
            Scalar(IntUnary(CountLeadingZeros)),
          ) {
          return None
        }
      I32Ctz => if !push_i32_ctz(builder, stack) { return None }
      I64Ctz =>
        if !push_unary_opcode(
            builder,
            stack,
            Scalar(IntUnary(CountTrailingZeros)),
          ) {
          return None
        }
      I32Popcnt => if !push_i32_popcnt(builder, stack) { return None }
      I64Popcnt =>
        if !push_unary_opcode(builder, stack, Scalar(IntUnary(PopulationCount))) {
          return None
        }
      I32Eqz => {
        guard pop_value(stack) is Some(value) else { return None }
        stack.push(builder.icmp_eq(value, builder.iconst_i32(0)))
      }
      I64Eqz => {
        guard pop_value(stack) is Some(value) else { return None }
        stack.push(builder.icmp_eq(value, builder.iconst_i64(0L)))
      }
      I32Eq | I64Eq => if !push_int_cmp(builder, stack, Eq) { return None }
      I32Ne | I64Ne => if !push_int_cmp(builder, stack, Ne) { return None }
      I32LtS | I64LtS => if !push_int_cmp(builder, stack, Slt) { return None }
      I32LtU | I64LtU => if !push_int_cmp(builder, stack, Ult) { return None }
      I32GtS | I64GtS => if !push_int_cmp(builder, stack, Sgt) { return None }
      I32GtU | I64GtU => if !push_int_cmp(builder, stack, Ugt) { return None }
      I32LeS | I64LeS => if !push_int_cmp(builder, stack, Sle) { return None }
      I32LeU | I64LeU => if !push_int_cmp(builder, stack, Ule) { return None }
      I32GeS | I64GeS => if !push_int_cmp(builder, stack, Sge) { return None }
      I32GeU | I64GeU => if !push_int_cmp(builder, stack, Uge) { return None }
      F32Add | F64Add =>
        if !push_binary(stack, fn(lhs, rhs) { builder.fadd(lhs, rhs) }) {
          return None
        }
      F32Sub | F64Sub =>
        if !push_binary(stack, fn(lhs, rhs) { builder.fsub(lhs, rhs) }) {
          return None
        }
      F32Mul | F64Mul =>
        if !push_binary(stack, fn(lhs, rhs) { builder.fmul(lhs, rhs) }) {
          return None
        }
      F32Div | F64Div =>
        if !push_binary_opcode(builder, stack, Scalar(FloatBinary(Div))) {
          return None
        }
      F32Min | F64Min =>
        if !push_binary_opcode(builder, stack, Scalar(FloatBinary(Min))) {
          return None
        }
      F32Max | F64Max =>
        if !push_binary_opcode(builder, stack, Scalar(FloatBinary(Max))) {
          return None
        }
      F32Copysign =>
        if !push_float_copysign(
            builder,
            stack,
            F32,
            I32,
            0x7FFFFFFFL,
            0x80000000U.reinterpret_as_int().to_int64(),
          ) {
          return None
        }
      F64Copysign =>
        if !push_float_copysign(
            builder,
            stack,
            F64,
            I64,
            0x7FFFFFFFFFFFFFFFL,
            0x8000000000000000UL.reinterpret_as_int64(),
          ) {
          return None
        }
      F32Neg | F64Neg =>
        if !push_unary_opcode(builder, stack, Scalar(FloatUnary(Neg))) {
          return None
        }
      F32Abs | F64Abs =>
        if !push_unary_opcode(builder, stack, Scalar(FloatUnary(Abs))) {
          return None
        }
      F32Sqrt | F64Sqrt =>
        if !push_unary_opcode(builder, stack, Scalar(FloatUnary(Sqrt))) {
          return None
        }
      F32Ceil | F64Ceil =>
        if !push_unary_opcode(builder, stack, Scalar(FloatUnary(Ceil))) {
          return None
        }
      F32Floor | F64Floor =>
        if !push_unary_opcode(builder, stack, Scalar(FloatUnary(Floor))) {
          return None
        }
      F32Trunc | F64Trunc =>
        if !push_unary_opcode(builder, stack, Scalar(FloatUnary(Trunc))) {
          return None
        }
      F32Nearest | F64Nearest =>
        if !push_unary_opcode(builder, stack, Scalar(FloatUnary(Nearest))) {
          return None
        }
      F32Eq | F64Eq => if !push_float_cmp(builder, stack, Eq) { return None }
      F32Ne | F64Ne => if !push_float_cmp(builder, stack, Ne) { return None }
      F32Lt | F64Lt => if !push_float_cmp(builder, stack, Lt) { return None }
      F32Gt | F64Gt => if !push_float_cmp(builder, stack, Gt) { return None }
      F32Le | F64Le => if !push_float_cmp(builder, stack, Le) { return None }
      F32Ge | F64Ge => if !push_float_cmp(builder, stack, Ge) { return None }
      I32WrapI64 =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I32,
            Scalar(Convert(IntReduce)),
          ) {
          return None
        }
      I64ExtendI32S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(Convert(SignedExtend)),
          ) {
          return None
        }
      I64ExtendI32U =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(Convert(UnsignedExtend)),
          ) {
          return None
        }
      I32Extend8S =>
        if !push_unary_opcode_as(builder, stack, I32, Scalar(SignExtendFrom(8))) {
          return None
        }
      I32Extend16S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I32,
            Scalar(SignExtendFrom(16)),
          ) {
          return None
        }
      I64Extend8S =>
        if !push_unary_opcode_as(builder, stack, I64, Scalar(SignExtendFrom(8))) {
          return None
        }
      I64Extend16S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(SignExtendFrom(16)),
          ) {
          return None
        }
      I64Extend32S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(SignExtendFrom(32)),
          ) {
          return None
        }
      F32DemoteF64 =>
        if !push_unary_opcode_as(
            builder,
            stack,
            F32,
            Scalar(Convert(FloatDemote)),
          ) {
          return None
        }
      F64PromoteF32 =>
        if !push_unary_opcode_as(
            builder,
            stack,
            F64,
            Scalar(Convert(FloatPromote)),
          ) {
          return None
        }
      I32TruncF32S | I32TruncF64S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I32,
            Scalar(Convert(FloatToSignedInt)),
          ) {
          return None
        }
      I32TruncF32U | I32TruncF64U =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I32,
            Scalar(Convert(FloatToUnsignedInt)),
          ) {
          return None
        }
      I64TruncF32S | I64TruncF64S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(Convert(FloatToSignedInt)),
          ) {
          return None
        }
      I64TruncF32U | I64TruncF64U =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(Convert(FloatToUnsignedInt)),
          ) {
          return None
        }
      F32ConvertI32S | F32ConvertI64S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            F32,
            Scalar(Convert(SignedIntToFloat)),
          ) {
          return None
        }
      F32ConvertI32U | F32ConvertI64U =>
        if !push_unary_opcode_as(
            builder,
            stack,
            F32,
            Scalar(Convert(UnsignedIntToFloat)),
          ) {
          return None
        }
      F64ConvertI32S | F64ConvertI64S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            F64,
            Scalar(Convert(SignedIntToFloat)),
          ) {
          return None
        }
      F64ConvertI32U | F64ConvertI64U =>
        if !push_unary_opcode_as(
            builder,
            stack,
            F64,
            Scalar(Convert(UnsignedIntToFloat)),
          ) {
          return None
        }
      I32TruncSatF32S | I32TruncSatF64S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I32,
            Scalar(Convert(FloatToSignedIntSaturating)),
          ) {
          return None
        }
      I32TruncSatF32U | I32TruncSatF64U =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I32,
            Scalar(Convert(FloatToUnsignedIntSaturating)),
          ) {
          return None
        }
      I64TruncSatF32S | I64TruncSatF64S =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(Convert(FloatToSignedIntSaturating)),
          ) {
          return None
        }
      I64TruncSatF32U | I64TruncSatF64U =>
        if !push_unary_opcode_as(
            builder,
            stack,
            I64,
            Scalar(Convert(FloatToUnsignedIntSaturating)),
          ) {
          return None
        }
      I32ReinterpretF32 =>
        if !push_unary_opcode_as(builder, stack, I32, Scalar(Convert(Bitcast))) {
          return None
        }
      I64ReinterpretF64 =>
        if !push_unary_opcode_as(builder, stack, I64, Scalar(Convert(Bitcast))) {
          return None
        }
      F32ReinterpretI32 =>
        if !push_unary_opcode_as(builder, stack, F32, Scalar(Convert(Bitcast))) {
          return None
        }
      F64ReinterpretI64 =>
        if !push_unary_opcode_as(builder, stack, F64, Scalar(Convert(Bitcast))) {
          return None
        }
      RefI31 => if !push_i31_new(builder, stack) { return None }
      I31GetS => if !push_i31_get(builder, stack, true) { return None }
      I31GetU => if !push_i31_get(builder, stack, false) { return None }
      AnyConvertExtern | ExternConvertAny =>
        if !push_unary_opcode(builder, stack, Scalar(Copy)) {
          return None
        }
      Call(func_idx) =>
        if !push_direct_call(builder, stack, env, mod_, vmctx, func_idx) {
          return None
        }
      I32Load(memidx, _, offset) =>
        if !push_memory_load(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I32,
            offset,
          ) {
          return None
        }
      I64Load(memidx, _, offset) =>
        if !push_memory_load(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            offset,
          ) {
          return None
        }
      F32Load(memidx, _, offset) =>
        if !push_memory_load(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            F32,
            offset,
          ) {
          return None
        }
      F64Load(memidx, _, offset) =>
        if !push_memory_load(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            F64,
            offset,
          ) {
          return None
        }
      I32Load8S(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I32,
            8,
            true,
            offset,
          ) {
          return None
        }
      I32Load8U(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I32,
            8,
            false,
            offset,
          ) {
          return None
        }
      I32Load16S(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I32,
            16,
            true,
            offset,
          ) {
          return None
        }
      I32Load16U(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I32,
            16,
            false,
            offset,
          ) {
          return None
        }
      I64Load8S(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            8,
            true,
            offset,
          ) {
          return None
        }
      I64Load8U(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            8,
            false,
            offset,
          ) {
          return None
        }
      I64Load16S(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            16,
            true,
            offset,
          ) {
          return None
        }
      I64Load16U(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            16,
            false,
            offset,
          ) {
          return None
        }
      I64Load32S(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            32,
            true,
            offset,
          ) {
          return None
        }
      I64Load32U(memidx, _, offset) =>
        if !push_memory_load_narrow(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            32,
            false,
            offset,
          ) {
          return None
        }
      I32Store(memidx, _, offset) =>
        if !push_memory_store(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I32,
            offset,
          ) {
          return None
        }
      I64Store(memidx, _, offset) =>
        if !push_memory_store(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            I64,
            offset,
          ) {
          return None
        }
      F32Store(memidx, _, offset) =>
        if !push_memory_store(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            F32,
            offset,
          ) {
          return None
        }
      F64Store(memidx, _, offset) =>
        if !push_memory_store(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            F64,
            offset,
          ) {
          return None
        }
      V128Load(memidx, _, offset) =>
        if !push_memory_load(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            V128,
            offset,
          ) {
          return None
        }
      V128Store(memidx, _, offset) =>
        if !push_memory_store(
            builder,
            stack,
            env,
            vmctx,
            mod_,
            memidx,
            V128,
            offset,
          ) {
          return None
        }
      I32Store8(memidx, _, offset) =>
        if !push_memory_store_narrow(
            builder, stack, env, vmctx, mod_, memidx, 8, offset,
          ) {
          return None
        }
      I32Store16(memidx, _, offset) =>
        if !push_memory_store_narrow(
            builder, stack, env, vmctx, mod_, memidx, 16, offset,
          ) {
          return None
        }
      I64Store8(memidx, _, offset) =>
        if !push_memory_store_narrow(
            builder, stack, env, vmctx, mod_, memidx, 8, offset,
          ) {
          return None
        }
      I64Store16(memidx, _, offset) =>
        if !push_memory_store_narrow(
            builder, stack, env, vmctx, mod_, memidx, 16, offset,
          ) {
          return None
        }
      I64Store32(memidx, _, offset) =>
        if !push_memory_store_narrow(
            builder, stack, env, vmctx, mod_, memidx, 32, offset,
          ) {
          return None
        }
      MemorySize(memidx) =>
        if !push_memory_size(builder, stack, env, vmctx, memory_types, memidx) {
          return None
        }
      MemoryGrow(memidx) =>
        if !push_memory_grow(
            builder, stack, runtime_symbols, vmctx, memory_types, memidx,
          ) {
          return None
        }
      MemoryFill(memidx) =>
        if !push_memory_fill(
            builder, stack, runtime_symbols, vmctx, memory_types, memidx,
          ) {
          return None
        }
      MemoryCopy(dst_memidx, src_memidx) =>
        if !push_memory_copy(
            builder, stack, runtime_symbols, vmctx, memory_types, dst_memidx, src_memidx,
          ) {
          return None
        }
      MemoryInit(memidx, data_idx) =>
        if !push_memory_init(
            builder, stack, runtime_symbols, vmctx, memory_types, memidx, data_idx,
          ) {
          return None
        }
      DataDrop(data_idx) =>
        if !push_data_drop(builder, runtime_symbols, vmctx, data_idx) {
          return None
        }
      TableGrow(table_idx) =>
        if !push_table_grow(
            builder, stack, runtime_symbols, vmctx, table_types, table_idx,
          ) {
          return None
        }
      TableFill(table_idx) =>
        if !push_table_fill(
            builder, stack, runtime_symbols, vmctx, table_types, table_idx,
          ) {
          return None
        }
      TableCopy(dst_table_idx, src_table_idx) =>
        if !push_table_copy(
            builder, stack, runtime_symbols, vmctx, table_types, dst_table_idx, src_table_idx,
          ) {
          return None
        }
      TableInit(table_idx, elem_idx) =>
        if !push_table_init(
            builder, stack, runtime_symbols, vmctx, table_types, table_idx, elem_idx,
          ) {
          return None
        }
      ElemDrop(elem_idx) =>
        if !push_elem_drop(builder, runtime_symbols, vmctx, elem_idx) {
          return None
        }
      RefNull(ref_type) =>
        if !push_ref_null(builder, stack, ref_type) {
          return None
        }
      RefIsNull => if !push_ref_is_null(builder, stack) { return None }
      RefFunc(func_idx) =>
        if !push_ref_func(builder, stack, env, vmctx, func_idx) {
          return None
        }
      RefAsNonNull => if !push_ref_as_non_null(builder, stack) { return None }
      RefEqInstr => if !push_ref_eq(builder, stack) { return None }
      Select | SelectTyped(_) => if !push_select(builder, stack) { return None }
      Drop => {
        guard pop_value(stack) is Some(_) else { return None }
      }
      Return => {
        if !finish_linear_return(builder, stack, func_type.results.length()) {
          return None
        }
        terminated = true
      }
      Unreachable => {
        builder.trap("unreachable")
        terminated = true
      }
      _ =>
        // TODO(wasm_frontend): port the remaining Wasm operators/control-flow
        // into native MilkIR construction.
        return None
    }
  }
  if !terminated &&
    !finish_linear_return(builder, stack, func_type.results.length()) {
    return None
  }
  Some(builder.get_function())
}