// IR Printer - Pretty prints IR in a readable text format
// Textual IR format for debugging

///|
/// Print a type
fn format_type(ty : Type) -> String {
  match ty {
    I32 => "i32"
    I64 => "i64"
    F32 => "f32"
    F64 => "f64"
    V128 => "v128"
    Ptr => "ptr"
    Ref => "ref"
    CallableRef => "callable_ref"
    OpaqueRef => "opaque_ref"
  }
}

///|
/// Print a value reference
fn format_value(v : Value) -> String {
  "v\{v.id}"
}

///|
/// Print an integer comparison code
fn format_intcc(cc : IntCC) -> String {
  match cc {
    Eq => "eq"
    Ne => "ne"
    Slt => "slt"
    Sle => "sle"
    Sgt => "sgt"
    Sge => "sge"
    Ult => "ult"
    Ule => "ule"
    Ugt => "ugt"
    Uge => "uge"
  }
}

///|
/// Print a float comparison code
fn format_floatcc(cc : FloatCC) -> String {
  match cc {
    Eq => "eq"
    Ne => "ne"
    Lt => "lt"
    Le => "le"
    Gt => "gt"
    Ge => "ge"
  }
}

///|
/// Print an opcode with its operands
/// result_ty is needed for Fconst to correctly decode F32 bit-packed constants
fn format_opcode(
  opcode : Opcode,
  operands : Array[Value],
  result_ty : Type?,
) -> String {
  let ops = operands.map(format_value).join(", ")
  match opcode {
    // Constants
    Iconst(n) => "iconst \{n}"
    Fconst(n) =>
      // For F32, the bits are packed in the lower 32 bits of the Double
      if result_ty is Some(F32) {
        let f32_bits = n.reinterpret_as_int64().to_int()
        let f32_val = Float::reinterpret_from_int(f32_bits)
        "fconst \{f32_val}"
      } else {
        "fconst \{n}"
      }

    // Integer arithmetic
    Iadd => "iadd \{ops}"
    Isub => "isub \{ops}"
    Imul => "imul \{ops}"
    Umulh => "umulh \{ops}"
    Smulh => "smulh \{ops}"
    Sdiv => "sdiv \{ops}"
    Udiv => "udiv \{ops}"
    Srem => "srem \{ops}"
    Urem => "urem \{ops}"

    // Bitwise operations
    Band => "band \{ops}"
    Bor => "bor \{ops}"
    Bxor => "bxor \{ops}"
    Bnot => "bnot \{ops}"
    Ishl => "ishl \{ops}"
    Sshr => "sshr \{ops}"
    Ushr => "ushr \{ops}"
    Rotl => "rotl \{ops}"
    Rotr => "rotr \{ops}"

    // Bit counting
    Clz => "clz \{ops}"
    Ctz => "ctz \{ops}"
    Popcnt => "popcnt \{ops}"

    // Comparisons
    Icmp(cc) => "icmp.\{format_intcc(cc)} \{ops}"
    IcmpEq => "icmp.eq \{ops}"

    // Floating point arithmetic
    Fadd => "fadd \{ops}"
    Fsub => "fsub \{ops}"
    Fmul => "fmul \{ops}"
    Fdiv => "fdiv \{ops}"
    Fmin => "fmin \{ops}"
    Fmax => "fmax \{ops}"

    // Floating point comparisons
    Fcmp(cc) => "fcmp.\{format_floatcc(cc)} \{ops}"

    // Floating point unary
    Fneg => "fneg \{ops}"
    Fabs => "fabs \{ops}"
    Fsqrt => "fsqrt \{ops}"
    Fceil => "fceil \{ops}"
    Ffloor => "ffloor \{ops}"
    Ftrunc => "ftrunc \{ops}"
    Fnearest => "fnearest \{ops}"

    // Conversions
    Ireduce => "ireduce \{ops}"
    Sextend => "sextend \{ops}"
    Uextend => "uextend \{ops}"
    Fpromote => "fpromote \{ops}"
    Fdemote => "fdemote \{ops}"
    FcvtToSint => "fcvt_to_sint \{ops}"
    FcvtToUint => "fcvt_to_uint \{ops}"
    FcvtToSintSat => "fcvt_to_sint_sat \{ops}"
    FcvtToUintSat => "fcvt_to_uint_sat \{ops}"
    SintToFcvt => "sint_to_fcvt \{ops}"
    UintToFcvt => "uint_to_fcvt \{ops}"
    Bitcast => "bitcast \{ops}"

    // In-place sign extension
    Sextend8 => "sextend8 \{ops}"
    Sextend16 => "sextend16 \{ops}"
    Sextend32 => "sextend32 \{ops}"

    // Misc
    Select => "select \{ops}"
    Copy => "copy \{ops}"
    Load => "load \{ops}"
    Store => "store \{ops}"
    StackAddr(slot) => "stack_addr slot\{slot.id}"

    // Function calls
    Call(symbol) => "call \{symbol.name}(\{ops})"
    CallIndirect(signature) => "call_indirect sig=\{to_repr(signature)}(\{ops})"

    // Raw pointer operations (for trampolines)
    LoadPtr(ty) => "load_ptr.\{ty} \{ops}"
    StorePtr(ty) => "store_ptr.\{ty} \{ops}"
    LoadPtrNarrow(result_ty, bits, signed) => {
      let sign_str = if signed { "s" } else { "u" }
      "load_ptr\{bits}_\{sign_str}.\{result_ty} \{ops}"
    }
    StorePtrNarrow(bits) => "store_ptr\{bits} \{ops}"
    CallPtr(num_args, num_results) =>
      "call_ptr(\{num_args}) -> \{num_results} (\{ops})"
    Trap(reason) => "trap \"\{reason}\" \{ops}"
    Custom(name) => "custom \{name} \{ops}"
    Ext(ext) => {
      let imms : Array[String] = []
      for imm in ext.immediates {
        imms.push("\{imm}")
      }
      let imm_str = if imms.length() == 0 {
        ""
      } else {
        " [" + imms.join(", ") + "]"
      }
      "ext.\{ext.dialect}.\{ext.opcode}\{imm_str} \{ops}"
    }

    // SIMD operations
    V128Const(_) => "v128.const \{ops}"
    V128Splat8 => "i8x16.splat \{ops}"
    V128Splat16 => "i16x8.splat \{ops}"
    V128Splat32 => "i32x4.splat \{ops}"
    V128Splat64 => "i64x2.splat \{ops}"
    V128SplatF32 => "f32x4.splat \{ops}"
    V128SplatF64 => "f64x2.splat \{ops}"
    V128ExtractLane8S(lane) => "i8x16.extract_lane_s \{lane} \{ops}"
    V128ExtractLane8U(lane) => "i8x16.extract_lane_u \{lane} \{ops}"
    V128ExtractLane16S(lane) => "i16x8.extract_lane_s \{lane} \{ops}"
    V128ExtractLane16U(lane) => "i16x8.extract_lane_u \{lane} \{ops}"
    V128ExtractLane32(lane) => "i32x4.extract_lane \{lane} \{ops}"
    V128ExtractLane64(lane) => "i64x2.extract_lane \{lane} \{ops}"
    V128ExtractLaneF32(lane) => "f32x4.extract_lane \{lane} \{ops}"
    V128ExtractLaneF64(lane) => "f64x2.extract_lane \{lane} \{ops}"
    V128ReplaceLane8(lane) => "i8x16.replace_lane \{lane} \{ops}"
    V128ReplaceLane16(lane) => "i16x8.replace_lane \{lane} \{ops}"
    V128ReplaceLane32(lane) => "i32x4.replace_lane \{lane} \{ops}"
    V128ReplaceLane64(lane) => "i64x2.replace_lane \{lane} \{ops}"
    V128ReplaceLaneF32(lane) => "f32x4.replace_lane \{lane} \{ops}"
    V128ReplaceLaneF64(lane) => "f64x2.replace_lane \{lane} \{ops}"
    V128Shuffle(_) => "i8x16.shuffle \{ops}"
    V128Swizzle => "i8x16.swizzle \{ops}"
    V128Not => "v128.not \{ops}"
    V128And => "v128.and \{ops}"
    V128AndNot => "v128.andnot \{ops}"
    V128Or => "v128.or \{ops}"
    V128Xor => "v128.xor \{ops}"
    V128Bitselect => "v128.bitselect \{ops}"
    V128AnyTrue => "v128.any_true \{ops}"
    V128AllTrue8 => "i8x16.all_true \{ops}"
    V128AllTrue16 => "i16x8.all_true \{ops}"
    V128AllTrue32 => "i32x4.all_true \{ops}"
    V128AllTrue64 => "i64x2.all_true \{ops}"
    V128Bitmask8 => "i8x16.bitmask \{ops}"
    V128Bitmask16 => "i16x8.bitmask \{ops}"
    V128Bitmask32 => "i32x4.bitmask \{ops}"
    V128Bitmask64 => "i64x2.bitmask \{ops}"
    V128Add8 => "i8x16.add \{ops}"
    V128Add16 => "i16x8.add \{ops}"
    V128Add32 => "i32x4.add \{ops}"
    V128Add64 => "i64x2.add \{ops}"
    V128Sub8 => "i8x16.sub \{ops}"
    V128Sub16 => "i16x8.sub \{ops}"
    V128Sub32 => "i32x4.sub \{ops}"
    V128Sub64 => "i64x2.sub \{ops}"
    V128Mul16 => "i16x8.mul \{ops}"
    V128Mul32 => "i32x4.mul \{ops}"
    V128Mul64 => "i64x2.mul \{ops}"
    V128AddSat8S => "i8x16.add_sat_s \{ops}"
    V128AddSat8U => "i8x16.add_sat_u \{ops}"
    V128AddSat16S => "i16x8.add_sat_s \{ops}"
    V128AddSat16U => "i16x8.add_sat_u \{ops}"
    V128SubSat8S => "i8x16.sub_sat_s \{ops}"
    V128SubSat8U => "i8x16.sub_sat_u \{ops}"
    V128SubSat16S => "i16x8.sub_sat_s \{ops}"
    V128SubSat16U => "i16x8.sub_sat_u \{ops}"
    V128Min8S => "i8x16.min_s \{ops}"
    V128Min8U => "i8x16.min_u \{ops}"
    V128Min16S => "i16x8.min_s \{ops}"
    V128Min16U => "i16x8.min_u \{ops}"
    V128Min32S => "i32x4.min_s \{ops}"
    V128Min32U => "i32x4.min_u \{ops}"
    V128Max8S => "i8x16.max_s \{ops}"
    V128Max8U => "i8x16.max_u \{ops}"
    V128Max16S => "i16x8.max_s \{ops}"
    V128Max16U => "i16x8.max_u \{ops}"
    V128Max32S => "i32x4.max_s \{ops}"
    V128Max32U => "i32x4.max_u \{ops}"
    V128Avgr8U => "i8x16.avgr_u \{ops}"
    V128Avgr16U => "i16x8.avgr_u \{ops}"
    V128Abs8 => "i8x16.abs \{ops}"
    V128Abs16 => "i16x8.abs \{ops}"
    V128Abs32 => "i32x4.abs \{ops}"
    V128Abs64 => "i64x2.abs \{ops}"
    V128Neg8 => "i8x16.neg \{ops}"
    V128Neg16 => "i16x8.neg \{ops}"
    V128Neg32 => "i32x4.neg \{ops}"
    V128Neg64 => "i64x2.neg \{ops}"
    V128Popcnt8 => "i8x16.popcnt \{ops}"
    V128Shl8 => "i8x16.shl \{ops}"
    V128Shl16 => "i16x8.shl \{ops}"
    V128Shl32 => "i32x4.shl \{ops}"
    V128Shl64 => "i64x2.shl \{ops}"
    V128Shr8S => "i8x16.shr_s \{ops}"
    V128Shr8U => "i8x16.shr_u \{ops}"
    V128Shr16S => "i16x8.shr_s \{ops}"
    V128Shr16U => "i16x8.shr_u \{ops}"
    V128Shr32S => "i32x4.shr_s \{ops}"
    V128Shr32U => "i32x4.shr_u \{ops}"
    V128Shr64S => "i64x2.shr_s \{ops}"
    V128Shr64U => "i64x2.shr_u \{ops}"
    V128Eq8 => "i8x16.eq \{ops}"
    V128Eq16 => "i16x8.eq \{ops}"
    V128Eq32 => "i32x4.eq \{ops}"
    V128Eq64 => "i64x2.eq \{ops}"
    V128Ne8 => "i8x16.ne \{ops}"
    V128Ne16 => "i16x8.ne \{ops}"
    V128Ne32 => "i32x4.ne \{ops}"
    V128Ne64 => "i64x2.ne \{ops}"
    V128Lt8S => "i8x16.lt_s \{ops}"
    V128Lt8U => "i8x16.lt_u \{ops}"
    V128Lt16S => "i16x8.lt_s \{ops}"
    V128Lt16U => "i16x8.lt_u \{ops}"
    V128Lt32S => "i32x4.lt_s \{ops}"
    V128Lt32U => "i32x4.lt_u \{ops}"
    V128Lt64S => "i64x2.lt_s \{ops}"
    V128Gt8S => "i8x16.gt_s \{ops}"
    V128Gt8U => "i8x16.gt_u \{ops}"
    V128Gt16S => "i16x8.gt_s \{ops}"
    V128Gt16U => "i16x8.gt_u \{ops}"
    V128Gt32S => "i32x4.gt_s \{ops}"
    V128Gt32U => "i32x4.gt_u \{ops}"
    V128Gt64S => "i64x2.gt_s \{ops}"
    V128Le8S => "i8x16.le_s \{ops}"
    V128Le8U => "i8x16.le_u \{ops}"
    V128Le16S => "i16x8.le_s \{ops}"
    V128Le16U => "i16x8.le_u \{ops}"
    V128Le32S => "i32x4.le_s \{ops}"
    V128Le32U => "i32x4.le_u \{ops}"
    V128Le64S => "i64x2.le_s \{ops}"
    V128Ge8S => "i8x16.ge_s \{ops}"
    V128Ge8U => "i8x16.ge_u \{ops}"
    V128Ge16S => "i16x8.ge_s \{ops}"
    V128Ge16U => "i16x8.ge_u \{ops}"
    V128Ge32S => "i32x4.ge_s \{ops}"
    V128Ge32U => "i32x4.ge_u \{ops}"
    V128Ge64S => "i64x2.ge_s \{ops}"
    V128Narrow16to8S => "i8x16.narrow_i16x8_s \{ops}"
    V128Narrow16to8U => "i8x16.narrow_i16x8_u \{ops}"
    V128Narrow32to16S => "i16x8.narrow_i32x4_s \{ops}"
    V128Narrow32to16U => "i16x8.narrow_i32x4_u \{ops}"
    V128ExtendLow8to16S => "i16x8.extend_low_i8x16_s \{ops}"
    V128ExtendHigh8to16S => "i16x8.extend_high_i8x16_s \{ops}"
    V128ExtendLow8to16U => "i16x8.extend_low_i8x16_u \{ops}"
    V128ExtendHigh8to16U => "i16x8.extend_high_i8x16_u \{ops}"
    V128ExtendLow16to32S => "i32x4.extend_low_i16x8_s \{ops}"
    V128ExtendHigh16to32S => "i32x4.extend_high_i16x8_s \{ops}"
    V128ExtendLow16to32U => "i32x4.extend_low_i16x8_u \{ops}"
    V128ExtendHigh16to32U => "i32x4.extend_high_i16x8_u \{ops}"
    V128ExtendLow32to64S => "i64x2.extend_low_i32x4_s \{ops}"
    V128ExtendHigh32to64S => "i64x2.extend_high_i32x4_s \{ops}"
    V128ExtendLow32to64U => "i64x2.extend_low_i32x4_u \{ops}"
    V128ExtendHigh32to64U => "i64x2.extend_high_i32x4_u \{ops}"
    V128ExtMulLow8to16S => "i16x8.extmul_low_i8x16_s \{ops}"
    V128ExtMulHigh8to16S => "i16x8.extmul_high_i8x16_s \{ops}"
    V128ExtMulLow8to16U => "i16x8.extmul_low_i8x16_u \{ops}"
    V128ExtMulHigh8to16U => "i16x8.extmul_high_i8x16_u \{ops}"
    V128ExtMulLow16to32S => "i32x4.extmul_low_i16x8_s \{ops}"
    V128ExtMulHigh16to32S => "i32x4.extmul_high_i16x8_s \{ops}"
    V128ExtMulLow16to32U => "i32x4.extmul_low_i16x8_u \{ops}"
    V128ExtMulHigh16to32U => "i32x4.extmul_high_i16x8_u \{ops}"
    V128ExtMulLow32to64S => "i64x2.extmul_low_i32x4_s \{ops}"
    V128ExtMulHigh32to64S => "i64x2.extmul_high_i32x4_s \{ops}"
    V128ExtMulLow32to64U => "i64x2.extmul_low_i32x4_u \{ops}"
    V128ExtMulHigh32to64U => "i64x2.extmul_high_i32x4_u \{ops}"
    V128ExtAddPairwise8to16S => "i16x8.extadd_pairwise_i8x16_s \{ops}"
    V128ExtAddPairwise8to16U => "i16x8.extadd_pairwise_i8x16_u \{ops}"
    V128ExtAddPairwise16to32S => "i32x4.extadd_pairwise_i16x8_s \{ops}"
    V128ExtAddPairwise16to32U => "i32x4.extadd_pairwise_i16x8_u \{ops}"
    V128Dot16to32S => "i32x4.dot_i16x8_s \{ops}"
    V128Q15MulrSat16S => "i16x8.q15mulr_sat_s \{ops}"
    V128AddF32 => "f32x4.add \{ops}"
    V128AddF64 => "f64x2.add \{ops}"
    V128SubF32 => "f32x4.sub \{ops}"
    V128SubF64 => "f64x2.sub \{ops}"
    V128MulF32 => "f32x4.mul \{ops}"
    V128MulF64 => "f64x2.mul \{ops}"
    V128DivF32 => "f32x4.div \{ops}"
    V128DivF64 => "f64x2.div \{ops}"
    V128MinF32 => "f32x4.min \{ops}"
    V128MinF64 => "f64x2.min \{ops}"
    V128MaxF32 => "f32x4.max \{ops}"
    V128MaxF64 => "f64x2.max \{ops}"
    V128PMinF32 => "f32x4.pmin \{ops}"
    V128PMinF64 => "f64x2.pmin \{ops}"
    V128PMaxF32 => "f32x4.pmax \{ops}"
    V128PMaxF64 => "f64x2.pmax \{ops}"
    V128AbsF32 => "f32x4.abs \{ops}"
    V128AbsF64 => "f64x2.abs \{ops}"
    V128NegF32 => "f32x4.neg \{ops}"
    V128NegF64 => "f64x2.neg \{ops}"
    V128SqrtF32 => "f32x4.sqrt \{ops}"
    V128SqrtF64 => "f64x2.sqrt \{ops}"
    V128CeilF32 => "f32x4.ceil \{ops}"
    V128CeilF64 => "f64x2.ceil \{ops}"
    V128FloorF32 => "f32x4.floor \{ops}"
    V128FloorF64 => "f64x2.floor \{ops}"
    V128TruncF32 => "f32x4.trunc \{ops}"
    V128TruncF64 => "f64x2.trunc \{ops}"
    V128NearestF32 => "f32x4.nearest \{ops}"
    V128NearestF64 => "f64x2.nearest \{ops}"
    V128EqF32 => "f32x4.eq \{ops}"
    V128EqF64 => "f64x2.eq \{ops}"
    V128NeF32 => "f32x4.ne \{ops}"
    V128NeF64 => "f64x2.ne \{ops}"
    V128LtF32 => "f32x4.lt \{ops}"
    V128LtF64 => "f64x2.lt \{ops}"
    V128GtF32 => "f32x4.gt \{ops}"
    V128GtF64 => "f64x2.gt \{ops}"
    V128LeF32 => "f32x4.le \{ops}"
    V128LeF64 => "f64x2.le \{ops}"
    V128GeF32 => "f32x4.ge \{ops}"
    V128GeF64 => "f64x2.ge \{ops}"
    V128TruncSatF32toI32S => "i32x4.trunc_sat_f32x4_s \{ops}"
    V128TruncSatF32toI32U => "i32x4.trunc_sat_f32x4_u \{ops}"
    V128TruncSatF64toI32SZero => "i32x4.trunc_sat_f64x2_s_zero \{ops}"
    V128TruncSatF64toI32UZero => "i32x4.trunc_sat_f64x2_u_zero \{ops}"
    V128ConvertI32toF32S => "f32x4.convert_i32x4_s \{ops}"
    V128ConvertI32toF32U => "f32x4.convert_i32x4_u \{ops}"
    V128ConvertLowI32toF64S => "f64x2.convert_low_i32x4_s \{ops}"
    V128ConvertLowI32toF64U => "f64x2.convert_low_i32x4_u \{ops}"
    V128DemoteF64toF32Zero => "f32x4.demote_f64x2_zero \{ops}"
    V128PromoteLowF32toF64 => "f64x2.promote_low_f32x4 \{ops}"
    V128Load8x8S(memidx, align, offset) =>
      "v128.load8x8_s mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load8x8U(memidx, align, offset) =>
      "v128.load8x8_u mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load16x4S(memidx, align, offset) =>
      "v128.load16x4_s mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load16x4U(memidx, align, offset) =>
      "v128.load16x4_u mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load32x2S(memidx, align, offset) =>
      "v128.load32x2_s mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load32x2U(memidx, align, offset) =>
      "v128.load32x2_u mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load8Splat(memidx, align, offset) =>
      "v128.load8_splat mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load16Splat(memidx, align, offset) =>
      "v128.load16_splat mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load32Splat(memidx, align, offset) =>
      "v128.load32_splat mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load64Splat(memidx, align, offset) =>
      "v128.load64_splat mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load32Zero(memidx, align, offset) =>
      "v128.load32_zero mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load64Zero(memidx, align, offset) =>
      "v128.load64_zero mem=\{memidx} align=\{align} offset=\{offset} \{ops}"
    V128Load8Lane(memidx, align, offset, lane) =>
      "v128.load8_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    V128Load16Lane(memidx, align, offset, lane) =>
      "v128.load16_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    V128Load32Lane(memidx, align, offset, lane) =>
      "v128.load32_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    V128Load64Lane(memidx, align, offset, lane) =>
      "v128.load64_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    V128Store8Lane(memidx, align, offset, lane) =>
      "v128.store8_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    V128Store16Lane(memidx, align, offset, lane) =>
      "v128.store16_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    V128Store32Lane(memidx, align, offset, lane) =>
      "v128.store32_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    V128Store64Lane(memidx, align, offset, lane) =>
      "v128.store64_lane mem=\{memidx} align=\{align} offset=\{offset} lane=\{lane} \{ops}"
    // Relaxed SIMD instructions
    V128RelaxedSwizzle => "v128.relaxed_swizzle \{ops}"
    V128RelaxedTruncF32toI32S => "v128.relaxed_trunc_f32_to_i32_s \{ops}"
    V128RelaxedTruncF32toI32U => "v128.relaxed_trunc_f32_to_i32_u \{ops}"
    V128RelaxedTruncF64toI32SZero =>
      "v128.relaxed_trunc_f64_to_i32_s_zero \{ops}"
    V128RelaxedTruncF64toI32UZero =>
      "v128.relaxed_trunc_f64_to_i32_u_zero \{ops}"
    V128RelaxedMaddF32 => "v128.relaxed_madd_f32 \{ops}"
    V128RelaxedNmaddF32 => "v128.relaxed_nmadd_f32 \{ops}"
    V128RelaxedMaddF64 => "v128.relaxed_madd_f64 \{ops}"
    V128RelaxedNmaddF64 => "v128.relaxed_nmadd_f64 \{ops}"
    V128RelaxedLaneselect8 => "v128.relaxed_laneselect8 \{ops}"
    V128RelaxedLaneselect16 => "v128.relaxed_laneselect16 \{ops}"
    V128RelaxedLaneselect32 => "v128.relaxed_laneselect32 \{ops}"
    V128RelaxedLaneselect64 => "v128.relaxed_laneselect64 \{ops}"
    V128RelaxedMinF32 => "v128.relaxed_min_f32 \{ops}"
    V128RelaxedMaxF32 => "v128.relaxed_max_f32 \{ops}"
    V128RelaxedMinF64 => "v128.relaxed_min_f64 \{ops}"
    V128RelaxedMaxF64 => "v128.relaxed_max_f64 \{ops}"
    V128RelaxedQ15MulrS => "v128.relaxed_q15mulr_s \{ops}"
    V128RelaxedDot8to16S => "v128.relaxed_dot_8_to_16_s \{ops}"
    V128RelaxedDot8to32AddS => "v128.relaxed_dot_8_to_32_add_s \{ops}"
  }
}

///|
/// Print an instruction
fn format_inst(inst : Inst) -> String {
  let result_ty = inst.first_result().map(fn(v) { v.ty })
  let opcode_str = format_opcode(inst.opcode, inst.operands, result_ty)
  if inst.results.length() == 0 {
    opcode_str
  } else if inst.results.length() == 1 {
    let r = inst.results[0]
    "\{format_value(r)}:\{format_type(r.ty)} = \{opcode_str}"
  } else {
    // Multi-value returns
    let all_results : Array[String] = []
    for r in inst.results {
      all_results.push(format_value(r) + ":" + format_type(r.ty))
    }
    let results_str = all_results.join(", ")
    "(\{results_str}) = \{opcode_str}"
  }
}

///|
/// Print a terminator
fn format_terminator(term : Terminator) -> String {
  match term {
    Jump(target, args) => {
      let args_str = args.map(format_value).join(", ")
      if args.length() > 0 {
        "jump block\{target}(\{args_str})"
      } else {
        "jump block\{target}"
      }
    }
    Brz(cond, then_block, else_block) =>
      "brz \{format_value(cond)}, block\{then_block}, block\{else_block}"
    Brnz(cond, then_block, else_block) =>
      "brnz \{format_value(cond)}, block\{then_block}, block\{else_block}"
    Branch(cond, true_block, true_args, false_block, false_args) => {
      let true_args_str = true_args.map(format_value).join(", ")
      let false_args_str = false_args.map(format_value).join(", ")
      "branch \{format_value(cond)}, block\{true_block}(\{true_args_str}), block\{false_block}(\{false_args_str})"
    }
    BrTable(index, targets, default_target) => {
      let targets_str = targets.map(fn(t) { "block\{t}" }).join(", ")
      "br_table \{format_value(index)}, [\{targets_str}], block\{default_target}"
    }
    Return(values) => {
      let vals_str = values.map(format_value).join(", ")
      if values.length() > 0 {
        "return \{vals_str}"
      } else {
        "return"
      }
    }
    Trap(reason) => "trap \"\{reason}\""
    TrapExit(reason) => "trap_exit \"\{reason}\""
  }
}

///|
/// Print a basic block
fn format_block(block : Block) -> String {
  let sb = StringBuilder::new()
  // Block header with parameters
  if block.params.length() > 0 {
    let params_str = block.params
      .map(fn(p) {
        let (v, ty) = p
        "\{format_value(v)}:\{format_type(ty)}"
      })
      .join(", ")
    sb.write_string("block\{block.id}(\{params_str}):\n")
  } else {
    sb.write_string("block\{block.id}:\n")
  }
  // Instructions
  for inst in block.instructions {
    sb.write_string("    \{format_inst(inst)}\n")
  }
  // Terminator
  match block.terminator {
    Some(term) => sb.write_string("    \{format_terminator(term)}\n")
    None => sb.write_string("    ; (no terminator)\n")
  }
  sb.to_string()
}

///|
/// Print a function
pub fn Function::print(self : Function) -> String {
  let sb = StringBuilder::new()
  // Function signature
  let params_str = self.params
    .map(fn(p) {
      let (v, ty) = p
      "\{format_value(v)}:\{format_type(ty)}"
    })
    .join(", ")
  let results_str = self.results.map(format_type).join(", ")
  if self.results.length() > 0 {
    sb.write_string(
      "function \{self.name}(\{params_str}) -> \{results_str} {\n",
    )
  } else {
    sb.write_string("function \{self.name}(\{params_str}) {\n")
  }
  // Blocks
  for block in self.blocks {
    sb.write_string(format_block(block))
  }
  sb.write_string("}\n")
  sb.to_string()
}

///|
/// Print using IRBuilder
pub fn IRBuilder::print(self : IRBuilder) -> String {
  self.func.print()
}