// Instruction translation - the main dispatch for WASM instructions
// Split from translator.mbt for maintainability

///|
/// Translate a single instruction
fn Translator::translate_instruction(
  self : Translator,
  instr : @types.Instruction,
) -> Unit {
  // If we're in unreachable code, only process control flow instructions
  // that may create new reachable regions
  if self.is_unreachable {
    match instr {
      Block(block_type, body) => self.translate_block(block_type, body)
      Loop(block_type, body) => self.translate_loop(block_type, body)
      If(block_type, then_body, else_body) =>
        self.translate_if(block_type, then_body, else_body)
      // All other instructions are dead code, skip them
      _ => return
    }
    return
  }
  match instr {
    Atomic(subopcode, memidx, _, offset) =>
      self.translate_atomic(subopcode, memidx, offset)

    // Constants
    I32Const(n) => {
      let v = self.builder.iconst_i32(n)
      self.push(v)
    }
    I64Const(n) => {
      let v = self.builder.iconst_i64(n)
      self.push(v)
    }
    F32Const(n) => {
      let v = self.builder.fconst_f32(n)
      self.push(v)
    }
    F64Const(n) => {
      let v = self.builder.fconst_f64(n)
      self.push(v)
    }

    // Local variables
    LocalGet(idx) => {
      let v = self.locals[idx]
      self.push(v)
    }
    LocalSet(idx) => {
      let v = self.pop()
      self.locals[idx] = v
    }
    LocalTee(idx) => {
      let v = self.peek()
      self.locals[idx] = v
    }

    // Stack operations
    Drop => self.pop() |> ignore
    Select => {
      let c = self.pop() // condition
      let val2 = self.pop() // false value
      let val1 = self.pop() // true value
      let result = self.builder.select(c, val1, val2)
      self.push(result)
    }
    SelectTyped(_) => {
      // Same as Select - type annotation is for validation only
      let c = self.pop() // condition
      let val2 = self.pop() // false value
      let val1 = self.pop() // true value
      let result = self.builder.select(c, val1, val2)
      self.push(result)
    }

    // i32 arithmetic
    I32Add => self.translate_binary_i32(fn(b, a, v) { b.iadd(a, v) })
    I32Sub => self.translate_binary_i32(fn(b, a, v) { b.isub(a, v) })
    I32Mul => self.translate_binary_i32(fn(b, a, v) { b.imul(a, v) })
    I32DivS => self.translate_binary_i32(fn(b, a, v) { b.sdiv(a, v) })
    I32DivU => self.translate_binary_i32(fn(b, a, v) { b.udiv(a, v) })
    I32RemS => self.translate_binary_i32(fn(b, a, v) { b.srem(a, v) })
    I32RemU => self.translate_binary_i32(fn(b, a, v) { b.urem(a, v) })
    I32And => self.translate_binary_i32(fn(b, a, v) { b.band(a, v) })
    I32Or => self.translate_binary_i32(fn(b, a, v) { b.bor(a, v) })
    I32Xor => self.translate_binary_i32(fn(b, a, v) { b.bxor(a, v) })
    I32Shl => self.translate_binary_i32(fn(b, a, v) { b.ishl(a, v) })
    I32ShrS => self.translate_binary_i32(fn(b, a, v) { b.sshr(a, v) })
    I32ShrU => self.translate_binary_i32(fn(b, a, v) { b.ushr(a, v) })
    I32Rotl => self.translate_binary_i32(fn(b, a, v) { b.rotl(a, v) })
    I32Rotr => self.translate_binary_i32(fn(b, a, v) { b.rotr(a, v) })

    // i32 bit counting
    I32Clz => self.translate_unary_i32(fn(b, a) { b.clz(a) })
    I32Ctz => self.translate_unary_i32(fn(b, a) { b.ctz(a) })
    I32Popcnt => self.translate_unary_i32(fn(b, a) { b.popcnt(a) })

    // i32 comparisons
    I32Eqz => {
      let a = self.pop()
      let zero = self.builder.iconst_i32(0)
      let result = self.builder.icmp_eq(a, zero)
      self.push(result)
    }
    I32Eq => self.translate_icmp(Eq)
    I32Ne => self.translate_icmp(Ne)
    I32LtS => self.translate_icmp(Slt)
    I32LtU => self.translate_icmp(Ult)
    I32GtS => self.translate_icmp(Sgt)
    I32GtU => self.translate_icmp(Ugt)
    I32LeS => self.translate_icmp(Sle)
    I32LeU => self.translate_icmp(Ule)
    I32GeS => self.translate_icmp(Sge)
    I32GeU => self.translate_icmp(Uge)

    // i64 arithmetic
    I64Add => self.translate_binary_i64(fn(b, a, v) { b.iadd(a, v) })
    I64Sub => self.translate_binary_i64(fn(b, a, v) { b.isub(a, v) })
    I64Mul => self.translate_binary_i64(fn(b, a, v) { b.imul(a, v) })
    I64MulWideU => {
      let b = self.pop()
      let a = self.pop()
      let lo = self.builder.imul(a, b)
      let hi = self.builder.umulh(a, b)
      self.push(lo)
      self.push(hi)
    }
    I64MulWideS => {
      let b = self.pop()
      let a = self.pop()
      let lo = self.builder.imul(a, b)
      let hi = self.builder.smulh(a, b)
      self.push(lo)
      self.push(hi)
    }
    I64DivS => self.translate_binary_i64(fn(b, a, v) { b.sdiv(a, v) })
    I64DivU => self.translate_binary_i64(fn(b, a, v) { b.udiv(a, v) })
    I64RemS => self.translate_binary_i64(fn(b, a, v) { b.srem(a, v) })
    I64RemU => self.translate_binary_i64(fn(b, a, v) { b.urem(a, v) })
    I64And => self.translate_binary_i64(fn(b, a, v) { b.band(a, v) })
    I64Or => self.translate_binary_i64(fn(b, a, v) { b.bor(a, v) })
    I64Xor => self.translate_binary_i64(fn(b, a, v) { b.bxor(a, v) })
    I64Shl => self.translate_binary_i64(fn(b, a, v) { b.ishl(a, v) })
    I64ShrS => self.translate_binary_i64(fn(b, a, v) { b.sshr(a, v) })
    I64ShrU => self.translate_binary_i64(fn(b, a, v) { b.ushr(a, v) })
    I64Rotl => self.translate_binary_i64(fn(b, a, v) { b.rotl(a, v) })
    I64Rotr => self.translate_binary_i64(fn(b, a, v) { b.rotr(a, v) })

    // i64 bit counting
    I64Clz => self.translate_unary_i64(fn(b, a) { b.clz(a) })
    I64Ctz => self.translate_unary_i64(fn(b, a) { b.ctz(a) })
    I64Popcnt => self.translate_unary_i64(fn(b, a) { b.popcnt(a) })

    // i64 comparisons
    I64Eqz => {
      let a = self.pop()
      let zero = self.builder.iconst_i64(0L)
      let result = self.builder.icmp_eq(a, zero)
      self.push(result)
    }
    I64Eq => self.translate_icmp(Eq)
    I64Ne => self.translate_icmp(Ne)
    I64LtS => self.translate_icmp(Slt)
    I64LtU => self.translate_icmp(Ult)
    I64GtS => self.translate_icmp(Sgt)
    I64GtU => self.translate_icmp(Ugt)
    I64LeS => self.translate_icmp(Sle)
    I64LeU => self.translate_icmp(Ule)
    I64GeS => self.translate_icmp(Sge)
    I64GeU => self.translate_icmp(Uge)

    // f32 arithmetic
    F32Add => self.translate_binary_f32(fn(b, a, v) { b.fadd(a, v) })
    F32Sub => self.translate_binary_f32(fn(b, a, v) { b.fsub(a, v) })
    F32Mul => self.translate_binary_f32(fn(b, a, v) { b.fmul(a, v) })
    F32Div => self.translate_binary_f32(fn(b, a, v) { b.fdiv(a, v) })
    F32Min => self.translate_binary_f32(fn(b, a, v) { b.fmin(a, v) })
    F32Max => self.translate_binary_f32(fn(b, a, v) { b.fmax(a, v) })
    F32Copysign => {
      // copysign(x, y) = magnitude of x with sign of y
      let y = self.pop() // sign source
      let x = self.pop() // magnitude source
      // Use bitwise operations to implement copysign
      // result_bits = (x_bits & 0x7FFFFFFF) | (y_bits & 0x80000000)
      let x_bits = self.builder.bitcast(I32, x)
      let y_bits = self.builder.bitcast(I32, y)
      let magnitude = self.builder.band(
        x_bits,
        self.builder.iconst_i32(0x7FFFFFFF),
      )
      let sign = self.builder.band(
        y_bits,
        self.builder.iconst_i32(0x80000000U.reinterpret_as_int()),
      )
      let result_bits = self.builder.bor(magnitude, sign)
      let result = self.builder.bitcast(F32, result_bits)
      self.push(result)
    }

    // f32 unary
    F32Neg => self.translate_unary_f32(fn(b, a) { b.fneg(a) })
    F32Abs => self.translate_unary_f32(fn(b, a) { b.fabs(a) })
    F32Sqrt => self.translate_unary_f32(fn(b, a) { b.fsqrt(a) })
    F32Ceil => self.translate_unary_f32(fn(b, a) { b.fceil(a) })
    F32Floor => self.translate_unary_f32(fn(b, a) { b.ffloor(a) })
    F32Trunc => self.translate_unary_f32(fn(b, a) { b.ftrunc(a) })
    F32Nearest => self.translate_unary_f32(fn(b, a) { b.fnearest(a) })

    // f32 comparisons
    F32Eq => self.translate_fcmp(Eq)
    F32Ne => self.translate_fcmp(Ne)
    F32Lt => self.translate_fcmp(Lt)
    F32Gt => self.translate_fcmp(Gt)
    F32Le => self.translate_fcmp(Le)
    F32Ge => self.translate_fcmp(Ge)

    // f64 arithmetic
    F64Add => self.translate_binary_f64(fn(b, a, v) { b.fadd(a, v) })
    F64Sub => self.translate_binary_f64(fn(b, a, v) { b.fsub(a, v) })
    F64Mul => self.translate_binary_f64(fn(b, a, v) { b.fmul(a, v) })
    F64Div => self.translate_binary_f64(fn(b, a, v) { b.fdiv(a, v) })
    F64Min => self.translate_binary_f64(fn(b, a, v) { b.fmin(a, v) })
    F64Max => self.translate_binary_f64(fn(b, a, v) { b.fmax(a, v) })
    F64Copysign => {
      // copysign(x, y) = magnitude of x with sign of y
      let y = self.pop() // sign source
      let x = self.pop() // magnitude source
      // Use bitwise operations to implement copysign
      // result_bits = (x_bits & 0x7FFFFFFFFFFFFFFF) | (y_bits & 0x8000000000000000)
      let x_bits = self.builder.bitcast(I64, x)
      let y_bits = self.builder.bitcast(I64, y)
      let magnitude = self.builder.band(
        x_bits,
        self.builder.iconst_i64(0x7FFFFFFFFFFFFFFFL),
      )
      let sign = self.builder.band(
        y_bits,
        self.builder.iconst_i64(0x8000000000000000UL.reinterpret_as_int64()),
      )
      let result_bits = self.builder.bor(magnitude, sign)
      let result = self.builder.bitcast(F64, result_bits)
      self.push(result)
    }

    // f64 unary
    F64Neg => self.translate_unary_f64(fn(b, a) { b.fneg(a) })
    F64Abs => self.translate_unary_f64(fn(b, a) { b.fabs(a) })
    F64Sqrt => self.translate_unary_f64(fn(b, a) { b.fsqrt(a) })
    F64Ceil => self.translate_unary_f64(fn(b, a) { b.fceil(a) })
    F64Floor => self.translate_unary_f64(fn(b, a) { b.ffloor(a) })
    F64Trunc => self.translate_unary_f64(fn(b, a) { b.ftrunc(a) })
    F64Nearest => self.translate_unary_f64(fn(b, a) { b.fnearest(a) })

    // f64 comparisons
    F64Eq => self.translate_fcmp(Eq)
    F64Ne => self.translate_fcmp(Ne)
    F64Lt => self.translate_fcmp(Lt)
    F64Gt => self.translate_fcmp(Gt)
    F64Le => self.translate_fcmp(Le)
    F64Ge => self.translate_fcmp(Ge)

    // Conversions
    I32WrapI64 => {
      let a = self.pop()
      let result = self.builder.ireduce(I32, a)
      self.push(result)
    }
    I64ExtendI32S => {
      let a = self.pop()
      let result = self.builder.sextend(I64, a)
      self.push(result)
    }
    I64ExtendI32U => {
      let a = self.pop()
      let result = self.builder.uextend(I64, a)
      self.push(result)
    }
    // In-place sign extension instructions 
    I32Extend8S => {
      let a = self.pop()
      let result = self.builder.sextend8(I32, a)
      self.push(result)
    }
    I32Extend16S => {
      let a = self.pop()
      let result = self.builder.sextend16(I32, a)
      self.push(result)
    }
    I64Extend8S => {
      let a = self.pop()
      let result = self.builder.sextend8(I64, a)
      self.push(result)
    }
    I64Extend16S => {
      let a = self.pop()
      let result = self.builder.sextend16(I64, a)
      self.push(result)
    }
    I64Extend32S => {
      let a = self.pop()
      let result = self.builder.sextend32(a)
      self.push(result)
    }
    F32DemoteF64 => {
      let a = self.pop()
      let result = self.builder.fdemote(a)
      self.push(result)
    }
    F64PromoteF32 => {
      let a = self.pop()
      let result = self.builder.fpromote(a)
      self.push(result)
    }
    I32TruncF32S | I32TruncF64S => {
      let a = self.pop()
      let result = self.builder.fcvt_to_sint(I32, a)
      self.push(result)
    }
    I32TruncF32U | I32TruncF64U => {
      let a = self.pop()
      let result = self.builder.fcvt_to_uint(I32, a)
      self.push(result)
    }
    I64TruncF32S | I64TruncF64S => {
      let a = self.pop()
      let result = self.builder.fcvt_to_sint(I64, a)
      self.push(result)
    }
    I64TruncF32U | I64TruncF64U => {
      let a = self.pop()
      let result = self.builder.fcvt_to_uint(I64, a)
      self.push(result)
    }
    F32ConvertI32S | F32ConvertI64S => {
      let a = self.pop()
      let result = self.builder.sint_to_fcvt(F32, a)
      self.push(result)
    }
    F32ConvertI32U | F32ConvertI64U => {
      let a = self.pop()
      let result = self.builder.uint_to_fcvt(F32, a)
      self.push(result)
    }
    F64ConvertI32S | F64ConvertI64S => {
      let a = self.pop()
      let result = self.builder.sint_to_fcvt(F64, a)
      self.push(result)
    }
    F64ConvertI32U | F64ConvertI64U => {
      let a = self.pop()
      let result = self.builder.uint_to_fcvt(F64, a)
      self.push(result)
    }
    // Saturating truncation operations
    I32TruncSatF32S | I32TruncSatF64S => {
      let a = self.pop()
      let result = self.builder.fcvt_to_sint_sat(I32, a)
      self.push(result)
    }
    I32TruncSatF32U | I32TruncSatF64U => {
      let a = self.pop()
      let result = self.builder.fcvt_to_uint_sat(I32, a)
      self.push(result)
    }
    I64TruncSatF32S | I64TruncSatF64S => {
      let a = self.pop()
      let result = self.builder.fcvt_to_sint_sat(I64, a)
      self.push(result)
    }
    I64TruncSatF32U | I64TruncSatF64U => {
      let a = self.pop()
      let result = self.builder.fcvt_to_uint_sat(I64, a)
      self.push(result)
    }
    I32ReinterpretF32
    | I64ReinterpretF64
    | F32ReinterpretI32
    | F64ReinterpretI64 => {
      let a = self.pop()
      let target_ty = match instr {
        I32ReinterpretF32 => Type::I32
        I64ReinterpretF64 => I64
        F32ReinterpretI32 => F32
        F64ReinterpretI64 => F64
        _ => I32
      }
      let result = self.builder.bitcast(target_ty, a)
      self.push(result)
    }

    // Memory operations (desugared via FuncEnvironment)
    // - memory 0 (memory32): relies on guard pages (no explicit bounds checks)
    // - others: use explicit bounds checks
    I32Load(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load(
        self.builder,
        self.vmctx,
        memidx,
        I32,
        addr,
        offset,
      )
      self.push(result)
    }
    I64Load(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        addr,
        offset,
      )
      self.push(result)
    }
    F32Load(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load(
        self.builder,
        self.vmctx,
        memidx,
        F32,
        addr,
        offset,
      )
      self.push(result)
    }
    F64Load(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load(
        self.builder,
        self.vmctx,
        memidx,
        F64,
        addr,
        offset,
      )
      self.push(result)
    }

    // Narrow load operations (desugared via FuncEnvironment)
    I32Load8S(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I32,
        8,
        true,
        addr,
        offset,
      )
      self.push(result)
    }
    I32Load8U(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I32,
        8,
        false,
        addr,
        offset,
      )
      self.push(result)
    }
    I32Load16S(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I32,
        16,
        true,
        addr,
        offset,
      )
      self.push(result)
    }
    I32Load16U(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I32,
        16,
        false,
        addr,
        offset,
      )
      self.push(result)
    }
    I64Load8S(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        8,
        true,
        addr,
        offset,
      )
      self.push(result)
    }
    I64Load8U(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        8,
        false,
        addr,
        offset,
      )
      self.push(result)
    }
    I64Load16S(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        16,
        true,
        addr,
        offset,
      )
      self.push(result)
    }
    I64Load16U(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        16,
        false,
        addr,
        offset,
      )
      self.push(result)
    }
    I64Load32S(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        32,
        true,
        addr,
        offset,
      )
      self.push(result)
    }
    I64Load32U(memidx, _, offset) => {
      let addr = self.pop()
      let result = self.func_env.translate_memory_load_narrow(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        32,
        false,
        addr,
        offset,
      )
      self.push(result)
    }

    // Store operations (desugared via FuncEnvironment to StorePtr with explicit bounds check)
    I32Store(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store(
        self.builder,
        self.vmctx,
        memidx,
        I32,
        addr,
        value,
        offset,
      )
    }
    I64Store(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store(
        self.builder,
        self.vmctx,
        memidx,
        I64,
        addr,
        value,
        offset,
      )
    }
    F32Store(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store(
        self.builder,
        self.vmctx,
        memidx,
        F32,
        addr,
        value,
        offset,
      )
    }
    F64Store(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store(
        self.builder,
        self.vmctx,
        memidx,
        F64,
        addr,
        value,
        offset,
      )
    }

    // Narrow store operations (desugared via FuncEnvironment)
    I32Store8(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store_narrow(
        self.builder,
        self.vmctx,
        memidx,
        8,
        addr,
        value,
        offset,
      )
    }
    I32Store16(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store_narrow(
        self.builder,
        self.vmctx,
        memidx,
        16,
        addr,
        value,
        offset,
      )
    }
    I64Store8(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store_narrow(
        self.builder,
        self.vmctx,
        memidx,
        8,
        addr,
        value,
        offset,
      )
    }
    I64Store16(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store_narrow(
        self.builder,
        self.vmctx,
        memidx,
        16,
        addr,
        value,
        offset,
      )
    }
    I64Store32(memidx, _, offset) => {
      let value = self.pop()
      let addr = self.pop()
      self.func_env.translate_memory_store_narrow(
        self.builder,
        self.vmctx,
        memidx,
        32,
        addr,
        value,
        offset,
      )
    }

    // Control flow
    Unreachable => {
      self.builder.trap("unreachable")
      self.is_unreachable = true
    }
    Nop => () // No operation
    Return => {
      let results = self.builder.get_function().results
      let return_vals : Array[Value] = []
      for _ in 0.. self.translate_block(block_type, body)
    Loop(block_type, body) => self.translate_loop(block_type, body)
    If(block_type, then_body, else_body) =>
      self.translate_if(block_type, then_body, else_body)
    Br(depth) => self.translate_br(depth)
    BrIf(depth) => self.translate_br_if(depth)
    BrTable(labels, default_) => self.translate_br_table(labels, default_)
    BrOnNull(depth) => self.translate_br_on_null(depth)
    BrOnNonNull(depth) => self.translate_br_on_non_null(depth)

    // Function calls - spill locals before call when inside try_table
    // because the callee might throw and we need caller's locals at call point
    Call(func_idx) => {
      self.spill_locals_if_in_try()
      self.translate_call(func_idx)
    }
    CallIndirect(type_idx, table_idx) => {
      self.spill_locals_if_in_try()
      self.translate_call_indirect(type_idx, table_idx)
    }
    CallRef(type_idx) => {
      self.spill_locals_if_in_try()
      self.translate_call_ref(type_idx)
    }
    ReturnCall(func_idx) => {
      self.spill_locals_if_in_try()
      self.unwind_try_handlers_for_tail_call()
      self.translate_return_call(func_idx)
    }
    ReturnCallIndirect(type_idx, table_idx) => {
      self.spill_locals_if_in_try()
      self.unwind_try_handlers_for_tail_call()
      self.translate_return_call_indirect(type_idx, table_idx)
    }
    ReturnCallRef(type_idx) => {
      self.spill_locals_if_in_try()
      self.unwind_try_handlers_for_tail_call()
      self.translate_return_call_ref(type_idx)
    }

    // Memory management (multi-memory support via memidx)
    // Note: These use IR opcodes instead of desugaring because they require
    // libcalls that are handled in the MachV emit phase
    MemoryGrow(memidx) => {
      let delta = self.pop()
      let is_mem64 = memidx < self.memory_is_64.length() &&
        self.memory_is_64[memidx]

      // Use the module/override maximum for memory 0.
      // For other memories, rely on the runtime descriptor's max.
      let max_pages = if memidx == 0 { self.memory_max } else { None }
      let result = match max_pages {
        Some(max) =>
          @wasm_milkir.memory_grow(
            self.builder,
            self.func_env.runtime_symbols,
            self.vmctx,
            memidx,
            delta,
            max_pages=max,
            is_memory64=is_mem64,
          )
        None =>
          @wasm_milkir.memory_grow(
            self.builder,
            self.func_env.runtime_symbols,
            self.vmctx,
            memidx,
            delta,
            is_memory64=is_mem64,
          )
      }
      self.push(result)
    }
    MemorySize(memidx) => {
      let is_mem64 = memidx < self.memory_is_64.length() &&
        self.memory_is_64[memidx]
      let result = @wasm_milkir.memory_size(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        memidx,
        is_memory64=is_mem64,
      )
      self.push(result)
    }
    MemoryFill(memidx) => {
      let n = self.pop() // size
      let val = self.pop() // value (byte)
      let d = self.pop() // destination
      @wasm_milkir.memory_fill(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        memidx,
        d,
        val,
        n,
      )
    }
    MemoryCopy(dst_memidx, src_memidx) => {
      let n = self.pop() // size
      let s = self.pop() // source
      let d = self.pop() // destination
      @wasm_milkir.memory_copy(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        dst_memidx,
        src_memidx,
        d,
        s,
        n,
      )
    }
    MemoryInit(memidx, data_idx) => {
      let n = self.pop() // size
      let s = self.pop() // source offset in data segment
      let d = self.pop() // destination offset in memory
      @wasm_milkir.memory_init(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        memidx,
        data_idx,
        d,
        s,
        n,
      )
    }
    DataDrop(data_idx) =>
      @wasm_milkir.data_drop(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        data_idx,
      )

    // Table bulk operations
    TableFill(table_idx) => {
      let n = self.pop() // size
      let val = self.pop() // value (ref)
      let d = self.pop() // destination
      @wasm_milkir.table_fill(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        table_idx,
        d,
        val,
        n,
      )
    }
    TableCopy(dst_table_idx, src_table_idx) => {
      let n = self.pop() // size
      let s = self.pop() // source
      let d = self.pop() // destination
      @wasm_milkir.table_copy(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        dst_table_idx,
        src_table_idx,
        d,
        s,
        n,
      )
    }
    TableInit(table_idx, elem_idx) => {
      let n = self.pop() // size
      let s = self.pop() // source offset in element segment
      let d = self.pop() // destination offset in table
      @wasm_milkir.table_init(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        table_idx,
        elem_idx,
        d,
        s,
        n,
      )
    }
    ElemDrop(elem_idx) =>
      @wasm_milkir.elem_drop(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        elem_idx,
      )

    // Reference types
    RefNull(ref_type) => {
      // Lowered reference encoding uses 0 for null.
      let result_ty = type_from_wasm(ref_type)
      let result = self.builder.iconst(result_ty, @wasm_milkir.NULL_REF)
      self.push(result)
    }
    RefIsNull => {
      let ref_val = self.pop()
      let null_sentinel = self.builder.iconst(ref_val.ty, @wasm_milkir.NULL_REF)
      let result = self.builder.icmp_eq(ref_val, null_sentinel)
      self.push(result)
    }
    RefFunc(func_idx) => {
      // Create a function reference - use GetFuncRef to get tagged function pointer
      // The pointer is tagged with FUNCREF_TAG (bit 61) for ref.test detection
      let result = @wasm_milkir.get_func_ref(
        self.builder,
        self.remap_func_idx(func_idx),
      )
      self.push(result)
    }
    RefAsNonNull => {
      // ref.as_non_null: convert nullable ref to non-null ref
      // Pop the reference, check if null, trap if so, otherwise push back
      let ref_val = self.pop()
      let null_sentinel = self.builder.iconst(ref_val.ty, @wasm_milkir.NULL_REF)
      let is_null = self.builder.icmp_eq(ref_val, null_sentinel)

      // Create trap block and continuation block
      let trap_block = self.builder.create_block()
      let continue_block = self.builder.create_block()

      // Branch: if is_null goto trap_block else continue_block
      self.builder.brnz(is_null, trap_block, continue_block)

      // Trap block: emit trap for null reference
      self.builder.switch_to_block(trap_block)
      self.builder.trap("null reference")

      // Continue block: push the non-null ref back on stack
      self.builder.switch_to_block(continue_block)
      self.push(ref_val)
    }
    RefEqInstr => {
      // ref.eq: compare two references for equality
      let ref2 = self.pop()
      let ref1 = self.pop()
      let result = @wasm_milkir.ref_eq(self.builder, ref1, ref2)
      self.push(result)
    }
    TableGet(table_idx) => {
      // Desugar to load from vmctx.tables[table_idx][elem_idx]
      let elem_idx = self.pop()
      let is_table64 = table_idx < self.table_is_64.length() &&
        self.table_is_64[table_idx]
      let result_type = self.table_elem_types[table_idx]
      let result = self.func_env.translate_table_get(
        self.builder,
        self.vmctx,
        table_idx,
        elem_idx,
        result_type,
        is_table64~,
      )
      self.push(result)
    }
    TableSet(table_idx) => {
      // Desugar to store to vmctx.tables[table_idx][elem_idx]
      let value = self.pop()
      let elem_idx = self.pop()
      let is_table64 = table_idx < self.table_is_64.length() &&
        self.table_is_64[table_idx]
      self.func_env.translate_table_set(
        self.builder,
        self.vmctx,
        table_idx,
        elem_idx,
        value,
        is_table64~,
      )
    }
    TableSize(table_idx) => {
      // Desugar to load from vmctx.table_sizes[table_idx]
      let is_table64 = table_idx < self.table_is_64.length() &&
        self.table_is_64[table_idx]
      let result = self.func_env.translate_table_size(
        self.builder,
        self.vmctx,
        table_idx,
        is_table64~,
      )
      self.push(result)
    }
    TableGrow(table_idx) => {
      // Note: Uses IR opcode instead of desugaring because it requires
      // a libcall that is handled in the MachV emit phase
      // Stack: [init_value, delta] -> [result]
      let delta = self.pop()
      let init_value = self.pop()
      let is_table64 = table_idx < self.table_is_64.length() &&
        self.table_is_64[table_idx]
      let result = @wasm_milkir.table_grow(
        self.builder,
        self.func_env.runtime_symbols,
        self.vmctx,
        table_idx,
        delta,
        init_value,
        is_table64~,
      )
      self.push(result)
    }
    GlobalGet(idx) => {
      // Desugar to load from vmctx.globals[idx]
      let result = self.func_env.translate_global_get(
        self.builder,
        self.vmctx,
        idx,
      )
      self.push(result)
    }
    GlobalSet(idx) => {
      // Desugar to store to vmctx.globals[idx]
      let value = self.pop()
      self.func_env.translate_global_set(self.builder, self.vmctx, idx, value)
    }

    // GC instructions - struct operations
    StructNew(type_idx) => {
      // Pop field values from stack (in reverse order)
      let field_count = self.get_struct_field_count(type_idx)
      let fields : Array[Value] = []
      for i = 0; i < field_count; i = i + 1 {
        fields.push(self.pop())
      }
      // Reverse to get correct order
      fields.rev_in_place()
      let result = @wasm_milkir.struct_new(self.builder, type_idx, fields)
      self.push(result)
    }
    StructNewDefault(type_idx) => {
      let result = @wasm_milkir.struct_new_default(self.builder, type_idx)
      self.push(result)
    }
    StructGet(type_idx, field_idx) => {
      let struct_ref = self.pop()
      let field_type = self.get_struct_field_ir_type(type_idx, field_idx)
      let result = @wasm_milkir.struct_get(
        self.builder,
        type_idx,
        field_idx,
        struct_ref,
        field_type,
      )
      self.push(result)
    }
    StructGetS(type_idx, field_idx) => {
      let struct_ref = self.pop()
      let byte_width = self.get_struct_field_byte_width(type_idx, field_idx)
      let result = @wasm_milkir.struct_get_s(
        self.builder,
        type_idx,
        field_idx,
        struct_ref,
        byte_width,
      )
      self.push(result)
    }
    StructGetU(type_idx, field_idx) => {
      let struct_ref = self.pop()
      let byte_width = self.get_struct_field_byte_width(type_idx, field_idx)
      let result = @wasm_milkir.struct_get_u(
        self.builder,
        type_idx,
        field_idx,
        struct_ref,
        byte_width,
      )
      self.push(result)
    }
    StructSet(type_idx, field_idx) => {
      let value = self.pop()
      let struct_ref = self.pop()
      @wasm_milkir.struct_set(
        self.builder,
        type_idx,
        field_idx,
        struct_ref,
        value,
      )
    }

    // GC instructions - array operations
    ArrayNew(type_idx) => {
      // Stack: [init_value, length] -> [arrayref]
      let length = self.pop()
      let init_value = self.pop()
      let result = @wasm_milkir.array_new(
        self.builder,
        type_idx,
        init_value,
        length,
      )
      self.push(result)
    }
    ArrayNewDefault(type_idx) => {
      // Stack: [length] -> [arrayref]
      let length = self.pop()
      let result = @wasm_milkir.array_new_default(
        self.builder,
        type_idx,
        length,
      )
      self.push(result)
    }
    ArrayNewFixed(type_idx, count) => {
      // Pop count values from stack
      let elements : Array[Value] = []
      for i = 0; i < count; i = i + 1 {
        elements.push(self.pop())
      }
      // Reverse to get correct order
      elements.rev_in_place()
      let result = @wasm_milkir.array_new_fixed(
        self.builder,
        type_idx,
        count,
        elements,
      )
      self.push(result)
    }
    ArrayGet(type_idx) => {
      // Stack: [arrayref, index] -> [value]
      let index = self.pop()
      let array_ref = self.pop()
      let elem_type = self.get_array_element_ir_type(type_idx)
      let result = @wasm_milkir.array_get(
        self.builder,
        type_idx,
        array_ref,
        index,
        elem_type,
      )
      self.push(result)
    }
    ArrayGetS(type_idx) => {
      let index = self.pop()
      let array_ref = self.pop()
      let byte_width = self.get_array_element_byte_width(type_idx)
      let result = @wasm_milkir.array_get_s(
        self.builder,
        type_idx,
        array_ref,
        index,
        byte_width,
      )
      self.push(result)
    }
    ArrayGetU(type_idx) => {
      let index = self.pop()
      let array_ref = self.pop()
      let byte_width = self.get_array_element_byte_width(type_idx)
      let result = @wasm_milkir.array_get_u(
        self.builder,
        type_idx,
        array_ref,
        index,
        byte_width,
      )
      self.push(result)
    }
    ArraySet(type_idx) => {
      // Stack: [arrayref, index, value] -> []
      let value = self.pop()
      let index = self.pop()
      let array_ref = self.pop()
      @wasm_milkir.array_set(self.builder, type_idx, array_ref, index, value)
    }
    ArrayLen => {
      let array_ref = self.pop()
      let result = @wasm_milkir.array_len(self.builder, array_ref)
      self.push(result)
    }
    ArrayFill(type_idx) => {
      // Stack: [arrayref, offset, value, count] -> []
      let count = self.pop()
      let value = self.pop()
      let offset = self.pop()
      let array_ref = self.pop()
      @wasm_milkir.array_fill(
        self.builder,
        type_idx,
        array_ref,
        offset,
        value,
        count,
      )
    }
    ArrayCopy(dst_type_idx, src_type_idx) => {
      // Stack: [dst, dst_offset, src, src_offset, count] -> []
      let count = self.pop()
      let src_offset = self.pop()
      let src = self.pop()
      let dst_offset = self.pop()
      let dst = self.pop()
      @wasm_milkir.array_copy(
        self.builder,
        dst_type_idx,
        src_type_idx,
        dst,
        dst_offset,
        src,
        src_offset,
        count,
      )
    }
    ArrayNewData(type_idx, data_idx) => {
      // Stack: [data_offset, length] -> [arrayref]
      let length = self.pop()
      let data_offset = self.pop()
      let result = @wasm_milkir.array_new_data(
        self.builder,
        type_idx,
        data_idx,
        data_offset,
        length,
      )
      self.push(result)
    }
    ArrayNewElem(type_idx, elem_idx) => {
      // Stack: [elem_offset, length] -> [arrayref]
      let length = self.pop()
      let elem_offset = self.pop()
      let result = @wasm_milkir.array_new_elem(
        self.builder,
        type_idx,
        elem_idx,
        elem_offset,
        length,
      )
      self.push(result)
    }
    ArrayInitData(type_idx, data_idx) => {
      // Stack: [arrayref, arr_offset, data_offset, length] -> []
      let length = self.pop()
      let data_offset = self.pop()
      let arr_offset = self.pop()
      let array_ref = self.pop()
      @wasm_milkir.array_init_data(
        self.builder,
        type_idx,
        data_idx,
        array_ref,
        arr_offset,
        data_offset,
        length,
      )
    }
    ArrayInitElem(type_idx, elem_idx) => {
      // Stack: [arrayref, arr_offset, elem_offset, length] -> []
      let length = self.pop()
      let elem_offset = self.pop()
      let arr_offset = self.pop()
      let array_ref = self.pop()
      @wasm_milkir.array_init_elem(
        self.builder,
        type_idx,
        elem_idx,
        array_ref,
        arr_offset,
        elem_offset,
        length,
      )
    }

    // GC instructions - i31 operations
    RefI31 => {
      let value = self.pop()
      let result = @wasm_milkir.i31_new(self.builder, value)
      self.push(result)
    }
    I31GetS => {
      let i31_ref = self.pop()
      let result = @wasm_milkir.i31_get_s(self.builder, i31_ref)
      self.push(result)
    }
    I31GetU => {
      let i31_ref = self.pop()
      let result = @wasm_milkir.i31_get_u(self.builder, i31_ref)
      self.push(result)
    }

    // GC instructions - type conversions
    AnyConvertExtern => {
      let extern_ref = self.pop()
      let result = @wasm_milkir.any_convert_extern(self.builder, extern_ref)
      self.push(result)
    }
    ExternConvertAny => {
      let any_ref = self.pop()
      let result = @wasm_milkir.extern_convert_any(self.builder, any_ref)
      self.push(result)
    }

    // Type testing/casting - these are more complex, need runtime support
    RefTest(value_type) => {
      let ref_val = self.pop()
      let type_idx = self.extract_type_idx(value_type)
      let result = @wasm_milkir.ref_test(self.builder, type_idx, false, ref_val)
      self.push(result)
    }
    RefTestNull(value_type) => {
      let ref_val = self.pop()
      let type_idx = self.extract_type_idx(value_type)
      let result = @wasm_milkir.ref_test(self.builder, type_idx, true, ref_val)
      self.push(result)
    }
    RefCast(value_type) => {
      let ref_val = self.pop()
      let type_idx = self.extract_type_idx(value_type)
      let result = @wasm_milkir.ref_cast(
        self.builder,
        type_idx,
        false,
        ref_val,
        type_from_wasm(value_type),
      )
      self.push(result)
    }
    RefCastNull(value_type) => {
      let ref_val = self.pop()
      let type_idx = self.extract_type_idx(value_type)
      let result = @wasm_milkir.ref_cast(
        self.builder,
        type_idx,
        true,
        ref_val,
        type_from_wasm(value_type),
      )
      self.push(result)
    }
    BrOnCast(label_depth, _from_type, to_type) =>
      self.translate_br_on_cast(label_depth, to_type)
    BrOnCastFail(label_depth, _from_type, to_type) =>
      self.translate_br_on_cast_fail(label_depth, to_type)

    // Exception handling
    // Uses setjmp/longjmp pattern in the JIT runtime
    Throw(tag_idx) => {
      // Get the tag's parameter types to know how many values to pop
      let tag_types = self.get_tag_param_types(tag_idx)

      // Pop exception values from stack, then restore declaration order.
      let values : Array[Value] = []
      for _ in 0.. {
      // Pop exnref from stack
      let exnref = self.pop()
      // Spill all locals before throw so catch handlers see throw-time values
      self.spill_locals_if_in_try()
      // Emit ThrowRef IR opcode
      @wasm_milkir.emit_throw_ref(self.builder, exnref)
      self.builder.trap("unreachable after throw_ref")
      self.is_unreachable = true
    }
    TryTable(block_type, handlers, body) => {
      // Exception handling via setjmp/longjmp
      //
      // The TryTableBegin opcode returns:
      // - 0 on normal entry (first setjmp return)
      // - non-zero on exception catch (longjmp return with handler_id)
      //
      // Flow:
      // 1. Call TryTableBegin (setjmp)
      // 2. If result != 0, branch to catch_dispatch
      // 3. Otherwise, execute try body
      // 4. In catch_dispatch: match tag and branch to handler

      let handler_id = self.block_stack.length() // Use stack depth as unique ID
      let result_types = get_block_result_types(block_type, self.func_types)

      // Create blocks for control flow
      let try_body_block = self.builder.create_block()
      let catch_dispatch = self.builder.create_block()
      let try_continuation = self.builder.create_block()
      for ty in result_types {
        self.builder.add_block_param(try_continuation, ty) |> ignore
      }

      // Emit TryTableBegin - sets up exception handler and calls setjmp
      let setjmp_result = @wasm_milkir.try_table_begin(self.builder, handler_id)

      // Branch based on setjmp result: 0 = normal, non-zero = exception caught
      let zero = self.builder.iconst_i32(0)
      let is_exception = self.builder.icmp_ne(setjmp_result, zero)
      self.builder.brnz(is_exception, catch_dispatch, try_body_block)

      // Save local types before translating body for proper restoration in catch
      let local_types : Array[Type] = []
      for loc in self.locals {
        local_types.push(loc.ty)
      }
      let num_locals = local_types.length()

      // === Try body block ===
      self.builder.switch_to_block(try_body_block)

      // Increment try_table_depth so spill_locals_if_in_try knows to spill
      // Note: decrement happens AFTER all catch handlers are translated,
      // so catch handlers can still spill for outer try_tables
      self.try_table_depth = self.try_table_depth + 1
      self.try_handler_stack.push(handler_id)

      // Translate the body using normal block translation
      self.translate_block(block_type, body)

      // Save post-body locals for the normal continuation path
      // (locals modified in try body should be visible after try_table)
      let post_body_locals = self.locals.copy()

      // Emit TryTableEnd - pops exception handler
      @wasm_milkir.try_table_end(self.builder, handler_id)

      // Jump to continuation after try body
      let normal_args : Array[Value] = []
      let blk = self.builder.current_block()
      let normal_falls_through = !self.is_unreachable && blk.terminator is None
      if normal_falls_through {
        for _ in 0.. {
            let lo = @wasm_milkir.get_spilled_local(self.builder, spill_idx)
            let hi = @wasm_milkir.get_spilled_local(self.builder, spill_idx + 1)
            spill_idx += 2
            self.locals.push(self.spill_words_to_v128(lo, hi))
          }
          ty => {
            let spilled_bits = @wasm_milkir.get_spilled_local(
              self.builder,
              spill_idx,
            )
            spill_idx += 1
            let restored_val = self.i64_bits_to_local(spilled_bits, ty)
            self.locals.push(restored_val)
          }
        }
      }

      // Get exception tag for matching
      let exn_tag = @wasm_milkir.get_exception_tag(self.builder)

      // Process each handler
      // Note: handlers reference labels in the OUTER block stack, not inside try_table
      for handler in handlers {
        match handler {
          Catch(tag_idx, label_depth) => {
            // Check if exception tag matches this handler
            let handler_tag = self.builder.iconst_i32(tag_idx)
            let tag_matches = self.builder.icmp_eq(exn_tag, handler_tag)

            // Create block for next handler check
            let next_check = self.builder.create_block()

            // If matches, branch to handler; otherwise continue checking
            let handler_block = self.builder.create_block()
            self.builder.brnz(tag_matches, handler_block, next_check)

            // In handler block: pop handler and branch to label
            self.builder.switch_to_block(handler_block)
            self.emit_catch_branch(tag_idx, label_depth, handler_id)
            self.builder.switch_to_block(next_check)
          }
          CatchRef(tag_idx, label_depth) => {
            // Same as Catch but also pushes exnref
            let handler_tag = self.builder.iconst_i32(tag_idx)
            let tag_matches = self.builder.icmp_eq(exn_tag, handler_tag)
            let next_check = self.builder.create_block()
            let handler_block = self.builder.create_block()
            self.builder.brnz(tag_matches, handler_block, next_check)
            self.builder.switch_to_block(handler_block)
            self.emit_catch_ref_branch(tag_idx, label_depth, handler_id)
            self.builder.switch_to_block(next_check)
          }
          CatchAll(label_depth) => {
            // Catch any exception - no tag check needed
            self.emit_catch_all_branch(label_depth, handler_id)
            // No need to continue checking after catch_all
            break
          }
          CatchAllRef(label_depth) => {
            // Catch any exception with exnref
            self.emit_catch_all_ref_branch(label_depth, handler_id)
            break
          }
        }
      }

      // If no handler matched, rethrow (delegate to outer handler)
      // Use depth=1 to skip this handler and propagate to the next outer one
      // (depth=0 would cause infinite loop by re-entering this same handler)
      let blk = self.builder.current_block()
      if blk.terminator is None {
        @wasm_milkir.delegate(self.builder, 1) // Propagate to outer handler
        self.builder.trap("unreachable after exception delegate")
      }

      // Decrement try_table_depth AFTER all catch handlers are translated
      // This ensures catch handlers can still spill for outer try_tables
      self.try_table_depth = self.try_table_depth - 1
      self.try_handler_stack.pop() |> ignore

      // === Continuation block ===
      self.builder.switch_to_block(try_continuation)

      // Restore post-body locals for the normal continuation path
      // This ensures locals modified in the try body are visible after try_table
      self.locals.clear()
      for loc in post_body_locals {
        self.locals.push(loc)
      }
      if normal_falls_through {
        for i, _ty in result_types {
          self.push(try_continuation.params[i].0)
        }
      } else {
        self.builder.trap("unreachable try_table continuation")
      }
      self.is_unreachable = !normal_falls_through
    }
    // ============ SIMD Instructions ============
    _ => self.translate_instruction_simd(instr)
  }
}

///|
fn Translator::translate_atomic(
  self : Translator,
  subopcode : Int,
  memidx : Int,
  offset : Int64,
) -> Unit {
  fn bounds_ptr(addr : Value, access_size : Int) -> Value {
    self.func_env.emit_bounds_check(
      self.builder,
      self.vmctx,
      memidx,
      addr,
      offset,
      access_size,
    )
  }

  fn align_check(addr : Value, access_size : Int) -> Unit {
    self.func_env.emit_atomic_alignment_check(
      self.builder,
      memidx,
      addr,
      offset,
      access_size,
    )
  }

  fn load_u32(bits : Int, addr : Value) -> Value {
    align_check(addr, bits / 8)
    let ptr = bounds_ptr(addr, bits / 8)
    let zero = self.builder.iconst(I64, 0L)
    self.builder.load_ptr_narrow(I32, bits, false, ptr, zero)
  }

  fn store_u32(bits : Int, addr : Value, value : Value) -> Unit {
    align_check(addr, bits / 8)
    let ptr = bounds_ptr(addr, bits / 8)
    let zero = self.builder.iconst(I64, 0L)
    self.builder.store_ptr_narrow(bits, ptr, value, zero)
  }

  fn load_u64(bits : Int, addr : Value) -> Value {
    align_check(addr, bits / 8)
    let ptr = bounds_ptr(addr, bits / 8)
    let zero = self.builder.iconst(I64, 0L)
    self.builder.load_ptr_narrow(I64, bits, false, ptr, zero)
  }

  fn store_u64(bits : Int, addr : Value, value : Value) -> Unit {
    align_check(addr, bits / 8)
    let ptr = bounds_ptr(addr, bits / 8)
    let zero = self.builder.iconst(I64, 0L)
    self.builder.store_ptr_narrow(bits, ptr, value, zero)
  }

  // NOTE: wasm-tools/wasmparser encodes most atomic subopcodes starting at 0x10.
  // The IR lowering table is indexed starting at 0x04, so shift accordingly.
  let op = if subopcode >= 0x10 { subopcode - 12 } else { subopcode }
  match op {
    // memory.atomic.notify
    0 => {
      let _count = self.pop()
      let addr = self.pop()
      align_check(addr, 4)
      ignore(bounds_ptr(addr, 4))
      self.push(self.builder.iconst_i32(0))
    }

    // memory.atomic.wait32
    1 => {
      let _timeout = self.pop()
      let expected = self.pop()
      let addr = self.pop()
      align_check(addr, 4)
      let ptr = bounds_ptr(addr, 4)
      let zero = self.builder.iconst(I64, 0L)
      let actual = self.builder.load_ptr(I32, ptr, zero)
      let eq = self.builder.icmp(Eq, actual, expected)
      let not_equal = self.builder.iconst_i32(1)
      let timed_out = self.builder.iconst_i32(2)
      let result = self.builder.select(eq, timed_out, not_equal)
      self.push(result)
    }

    // memory.atomic.wait64
    2 => {
      let _timeout = self.pop()
      let expected = self.pop()
      let addr = self.pop()
      align_check(addr, 8)
      let ptr = bounds_ptr(addr, 8)
      let zero = self.builder.iconst(I64, 0L)
      let actual = self.builder.load_ptr(I64, ptr, zero)
      let eq = self.builder.icmp(Eq, actual, expected)
      let not_equal = self.builder.iconst_i32(1)
      let timed_out = self.builder.iconst_i32(2)
      let result = self.builder.select(eq, timed_out, not_equal)
      self.push(result)
    }

    // atomic.fence
    3 => ()

    // Loads
    4 => {
      let addr = self.pop()
      align_check(addr, 4)
      let ptr = bounds_ptr(addr, 4)
      let zero = self.builder.iconst(I64, 0L)
      self.push(self.builder.load_ptr(I32, ptr, zero))
    }
    5 => {
      let addr = self.pop()
      align_check(addr, 8)
      let ptr = bounds_ptr(addr, 8)
      let zero = self.builder.iconst(I64, 0L)
      self.push(self.builder.load_ptr(I64, ptr, zero))
    }
    6 => {
      let addr = self.pop()
      self.push(load_u32(8, addr))
    }
    7 => {
      let addr = self.pop()
      self.push(load_u32(16, addr))
    }
    8 => {
      let addr = self.pop()
      self.push(load_u64(8, addr))
    }
    9 => {
      let addr = self.pop()
      self.push(load_u64(16, addr))
    }
    10 => {
      let addr = self.pop()
      self.push(load_u64(32, addr))
    }

    // Stores
    11 => {
      let value = self.pop()
      let addr = self.pop()
      align_check(addr, 4)
      let ptr = bounds_ptr(addr, 4)
      let zero = self.builder.iconst(I64, 0L)
      self.builder.store_ptr(I32, ptr, value, zero)
    }
    12 => {
      let value = self.pop()
      let addr = self.pop()
      align_check(addr, 8)
      let ptr = bounds_ptr(addr, 8)
      let zero = self.builder.iconst(I64, 0L)
      self.builder.store_ptr(I64, ptr, value, zero)
    }
    13 => {
      let value = self.pop()
      let addr = self.pop()
      store_u32(8, addr, value)
    }
    14 => {
      let value = self.pop()
      let addr = self.pop()
      store_u32(16, addr, value)
    }
    15 => {
      let value = self.pop()
      let addr = self.pop()
      store_u64(8, addr, value)
    }
    16 => {
      let value = self.pop()
      let addr = self.pop()
      store_u64(16, addr, value)
    }
    17 => {
      let value = self.pop()
      let addr = self.pop()
      store_u64(32, addr, value)
    }

    // i32 RMW operations
    18
    | 20
    | 21
    | 25
    | 27
    | 28
    | 32
    | 34
    | 35
    | 39
    | 41
    | 42
    | 46
    | 48
    | 49
    | 53
    | 55
    | 56 => {
      let value = self.pop()
      let addr = self.pop()
      let bits = if subopcode == 18 ||
        subopcode == 25 ||
        subopcode == 32 ||
        subopcode == 39 ||
        subopcode == 46 ||
        subopcode == 53 {
        32
      } else if subopcode == 20 ||
        subopcode == 27 ||
        subopcode == 34 ||
        subopcode == 41 ||
        subopcode == 48 ||
        subopcode == 55 {
        8
      } else {
        16
      }
      let access_size = bits / 8
      align_check(addr, access_size)
      let ptr = bounds_ptr(addr, access_size)
      let zero = self.builder.iconst(I64, 0L)
      let old = if bits == 32 {
        self.builder.load_ptr(I32, ptr, zero)
      } else {
        self.builder.load_ptr_narrow(I32, bits, false, ptr, zero)
      }
      let mask = if bits == 32 {
        self.builder.iconst_i32(-1)
      } else if bits == 16 {
        self.builder.iconst_i32(0xFFFF)
      } else {
        self.builder.iconst_i32(0xFF)
      }
      let v_masked = if bits == 32 {
        value
      } else {
        self.builder.band(value, mask)
      }
      let computed = match subopcode {
        18 | 20 | 21 => self.builder.iadd(old, v_masked)
        25 | 27 | 28 => self.builder.isub(old, v_masked)
        32 | 34 | 35 => self.builder.band(old, v_masked)
        39 | 41 | 42 => self.builder.bor(old, v_masked)
        46 | 48 | 49 => self.builder.bxor(old, v_masked)
        53 | 55 | 56 => v_masked
        _ => old
      }
      let new = if bits == 32 {
        computed
      } else {
        self.builder.band(computed, mask)
      }
      if bits == 32 {
        self.builder.store_ptr(I32, ptr, new, zero)
      } else {
        self.builder.store_ptr_narrow(bits, ptr, new, zero)
      }
      self.push(old)
    }

    // i64 RMW operations
    19
    | 22
    | 23
    | 24
    | 26
    | 29
    | 30
    | 31
    | 33
    | 36
    | 37
    | 38
    | 40
    | 43
    | 44
    | 45
    | 47
    | 50
    | 51
    | 52
    | 54
    | 57
    | 58
    | 59 => {
      let value = self.pop()
      let addr = self.pop()
      let bits = if subopcode == 19 ||
        subopcode == 26 ||
        subopcode == 33 ||
        subopcode == 40 ||
        subopcode == 47 ||
        subopcode == 54 {
        64
      } else if subopcode == 22 ||
        subopcode == 29 ||
        subopcode == 36 ||
        subopcode == 43 ||
        subopcode == 50 ||
        subopcode == 57 {
        8
      } else if subopcode == 23 ||
        subopcode == 30 ||
        subopcode == 37 ||
        subopcode == 44 ||
        subopcode == 51 ||
        subopcode == 58 {
        16
      } else {
        32
      }
      let access_size = bits / 8
      align_check(addr, access_size)
      let ptr = bounds_ptr(addr, access_size)
      let zero = self.builder.iconst(I64, 0L)
      let old = if bits == 64 {
        self.builder.load_ptr(I64, ptr, zero)
      } else {
        self.builder.load_ptr_narrow(I64, bits, false, ptr, zero)
      }
      let mask = if bits == 64 {
        self.builder.iconst_i64(-1L)
      } else if bits == 32 {
        self.builder.iconst_i64(0xFFFF_FFFFL)
      } else if bits == 16 {
        self.builder.iconst_i64(0xFFFFL)
      } else {
        self.builder.iconst_i64(0xFFL)
      }
      let v_masked = if bits == 64 {
        value
      } else {
        self.builder.band(value, mask)
      }
      let computed = match subopcode {
        19 | 22 | 23 | 24 => self.builder.iadd(old, v_masked)
        26 | 29 | 30 | 31 => self.builder.isub(old, v_masked)
        33 | 36 | 37 | 38 => self.builder.band(old, v_masked)
        40 | 43 | 44 | 45 => self.builder.bor(old, v_masked)
        47 | 50 | 51 | 52 => self.builder.bxor(old, v_masked)
        54 | 57 | 58 | 59 => v_masked
        _ => old
      }
      let new = if bits == 64 {
        computed
      } else {
        self.builder.band(computed, mask)
      }
      if bits == 64 {
        self.builder.store_ptr(I64, ptr, new, zero)
      } else {
        self.builder.store_ptr_narrow(bits, ptr, new, zero)
      }
      self.push(old)
    }

    // i32 cmpxchg
    60 | 62 | 63 => {
      let replacement = self.pop()
      let expected = self.pop()
      let addr = self.pop()
      let bits = if subopcode == 60 {
        32
      } else if subopcode == 62 {
        8
      } else {
        16
      }
      let access_size = bits / 8
      align_check(addr, access_size)
      let ptr = bounds_ptr(addr, access_size)
      let zero = self.builder.iconst(I64, 0L)
      let old = if bits == 32 {
        self.builder.load_ptr(I32, ptr, zero)
      } else {
        self.builder.load_ptr_narrow(I32, bits, false, ptr, zero)
      }
      let mask = if bits == 32 {
        self.builder.iconst_i32(-1)
      } else if bits == 16 {
        self.builder.iconst_i32(0xFFFF)
      } else {
        self.builder.iconst_i32(0xFF)
      }
      let expected_m = if bits == 32 {
        expected
      } else {
        self.builder.band(expected, mask)
      }
      let replacement_m = if bits == 32 {
        replacement
      } else {
        self.builder.band(replacement, mask)
      }
      let eq = self.builder.icmp(Eq, old, expected_m)
      let store_block = self.builder.create_block()
      let cont_block = self.builder.create_block()
      self.builder.brnz(eq, store_block, cont_block)
      self.builder.switch_to_block(store_block)
      if bits == 32 {
        self.builder.store_ptr(I32, ptr, replacement_m, zero)
      } else {
        self.builder.store_ptr_narrow(bits, ptr, replacement_m, zero)
      }
      self.builder.jump(cont_block, [])
      self.builder.switch_to_block(cont_block)
      self.push(old)
    }

    // i64 cmpxchg
    61 | 64 | 65 | 66 => {
      let replacement = self.pop()
      let expected = self.pop()
      let addr = self.pop()
      let bits = if subopcode == 61 {
        64
      } else if subopcode == 64 {
        8
      } else if subopcode == 65 {
        16
      } else {
        32
      }
      let access_size = bits / 8
      align_check(addr, access_size)
      let ptr = bounds_ptr(addr, access_size)
      let zero = self.builder.iconst(I64, 0L)
      let old = if bits == 64 {
        self.builder.load_ptr(I64, ptr, zero)
      } else {
        self.builder.load_ptr_narrow(I64, bits, false, ptr, zero)
      }
      let mask = if bits == 64 {
        self.builder.iconst_i64(-1L)
      } else if bits == 32 {
        self.builder.iconst_i64(0xFFFF_FFFFL)
      } else if bits == 16 {
        self.builder.iconst_i64(0xFFFFL)
      } else {
        self.builder.iconst_i64(0xFFL)
      }
      let expected_m = if bits == 64 {
        expected
      } else {
        self.builder.band(expected, mask)
      }
      let replacement_m = if bits == 64 {
        replacement
      } else {
        self.builder.band(replacement, mask)
      }
      let eq = self.builder.icmp(Eq, old, expected_m)
      let store_block = self.builder.create_block()
      let cont_block = self.builder.create_block()
      self.builder.brnz(eq, store_block, cont_block)
      self.builder.switch_to_block(store_block)
      if bits == 64 {
        self.builder.store_ptr(I64, ptr, replacement_m, zero)
      } else {
        self.builder.store_ptr_narrow(bits, ptr, replacement_m, zero)
      }
      self.builder.jump(cont_block, [])
      self.builder.switch_to_block(cont_block)
      self.push(old)
    }
    _ => {
      self.builder.trap("unsupported atomic")
      self.is_unreachable = true
    }
  }
}