// Lowering a cast.
//
// Ported from the `Cast` case of wax/src/lib-conversion/to_wasm.ml.
//
// `e as t` is one syntax over three unrelated jobs, and which one it is depends
// entirely on the pair of types involved:
//
//   * between two numeric types it is a CONVERSION, and the instruction is a
//     table entry over (from, to) -- widen, narrow, or change family;
//   * between two reference types it is a `ref.cast`, which checks at run time
//     and traps;
//   * from a numeric type to the same numeric type it is nothing at all, which
//     is worth emitting nothing for rather than an identity instruction.
//
// The float-to-integer direction has two forms the syntax spells differently:
// `as i32_s` saturates and `as i32_s_strict` traps. They are different
// instructions, and picking the wrong one is a module that silently computes
// something else at the edges.

///|
/// The instruction a numeric conversion becomes, or `None` when the pair has no
/// conversion at all.
///
/// An identity pair yields `Some([])`: the cast is real in the source -- it
/// pins a width the checker used -- and nothing at all in the output.
fn numeric_cast(
  from : @wasm_types.ValType[Int],
  to : @wasm_types.ValType[Int],
) -> Array[@wasm_bin.Instruction]? {
  if from == to {
    return Some([])
  }
  match (from, to) {
    (I64, I32) => Some([I32WrapI64])
    // A widening integer cast is SIGNED: wax `as i64` on an i32 means the
    // value, and the unsigned form is written `as i64_u`, which reaches here as
    // a `Signed` cast rather than this one.
    (I32, I64) => Some([I64ExtendI32S])
    (F64, F32) => Some([F32DemoteF64])
    (F32, F64) => Some([F64PromoteF32])
    (I32, F32) => Some([F32ConvertI32S])
    (I32, F64) => Some([F64ConvertI32S])
    (I64, F32) => Some([F32ConvertI64S])
    (I64, F64) => Some([F64ConvertI64S])
    // Float to integer without a written signedness has no instruction: the
    // source has to say `as i32_s` or `as i32_u`, and the checker has said so.
    (F32, I32) | (F32, I64) | (F64, I32) | (F64, I64) => None
    _ => None
  }
}

///|
/// The instruction a `Signed` cast becomes: a float-to-integer conversion, or a
/// widening of an integer with the signedness written out.
///
/// `strict` is the trapping form; without it the saturating one. They differ
/// only at the edges, which is exactly where a wrong choice is hardest to
/// notice.
fn signed_cast(
  from : @wasm_types.ValType[Int],
  to : @ast.NumType,
  signage : @wasm_types.Signage,
  strict : Bool,
) -> Array[@wasm_bin.Instruction]? {
  match (from, to, signage, strict) {
    (F32, I32, Signed, true) => Some([I32TruncF32S])
    (F32, I32, Unsigned, true) => Some([I32TruncF32U])
    (F64, I32, Signed, true) => Some([I32TruncF64S])
    (F64, I32, Unsigned, true) => Some([I32TruncF64U])
    (F32, I64, Signed, true) => Some([I64TruncF32S])
    (F32, I64, Unsigned, true) => Some([I64TruncF32U])
    (F64, I64, Signed, true) => Some([I64TruncF64S])
    (F64, I64, Unsigned, true) => Some([I64TruncF64U])
    (F32, I32, Signed, false) => Some([I32TruncSatF32S])
    (F32, I32, Unsigned, false) => Some([I32TruncSatF32U])
    (F64, I32, Signed, false) => Some([I32TruncSatF64S])
    (F64, I32, Unsigned, false) => Some([I32TruncSatF64U])
    (F32, I64, Signed, false) => Some([I64TruncSatF32S])
    (F32, I64, Unsigned, false) => Some([I64TruncSatF32U])
    (F64, I64, Signed, false) => Some([I64TruncSatF64S])
    (F64, I64, Unsigned, false) => Some([I64TruncSatF64U])
    // An integer widened with its signedness written out.
    (I32, I64, Signed, _) => Some([I64ExtendI32S])
    (I32, I64, Unsigned, _) => Some([I64ExtendI32U])
    // An integer converted to a float with its signedness written out.
    (I32, F32, Signed, _) => Some([F32ConvertI32S])
    (I32, F32, Unsigned, _) => Some([F32ConvertI32U])
    (I32, F64, Signed, _) => Some([F64ConvertI32S])
    (I32, F64, Unsigned, _) => Some([F64ConvertI32U])
    (I64, F32, Signed, _) => Some([F32ConvertI64S])
    (I64, F32, Unsigned, _) => Some([F32ConvertI64U])
    (I64, F64, Signed, _) => Some([F64ConvertI64S])
    (I64, F64, Unsigned, _) => Some([F64ConvertI64U])
    // A narrowing needs no sign: the bits are simply dropped.
    (I64, I32, _, _) => Some([I32WrapI64])
    _ => None
  }
}

///|
/// Emit whatever must happen BEFORE a cast's own instruction, and report the
/// type the value then has.
///
/// Several wax casts are two wasm instructions, because the source names only
/// the destination and the route there passes through a forced intermediate.
/// `r as i64_s` on a reference is `i31.get_s` and then a widening; `n as
/// &extern` on an i64 wraps to an i32, boxes it as an i31, and only then
/// converts. Emitting the intermediate here -- and reporting the type it
/// leaves -- is what lets the single match below finish every one of them the
/// same way the equivalent double cast would.
fn cast_prologue(
  out : Array[@wasm_bin.Instruction],
  from : @wasm_types.ValType[Int],
  target : @ast.CastType,
) -> @wasm_types.ValType[Int] {
  let i31ref : @wasm_types.RefType[Int] = { nullable: false, typ: I31 }
  match (from, target) {
    // `r as i32_s/u`: an i31 is already one, and anything else in the `any`
    // hierarchy is cast to one first. The `i31.get` follows in the match.
    (Ref({ typ: I31, .. }), Signed(typ=I32, ..)) => from
    (Ref(_), Signed(typ=I32, ..)) => {
      out.push(RefCast(i31ref))
      from
    }
    // `r as i64_s/u`: the same, and then the i32 it yields is widened.
    (Ref({ typ: I31, .. }), Signed(typ=I64, signage~, ..)) => {
      out.push(i31_get(signage))
      I32
    }
    (Ref(_), Signed(typ=I64, signage~, ..)) => {
      out.push(RefCast(i31ref))
      out.push(i31_get(signage))
      I32
    }
    // `ref.i31` takes an i32, so an i64 wraps first.
    (I64, Value(Ref({ typ: I31, .. }))) => {
      out.push(I32WrapI64)
      I32
    }
    // Crossing into the `extern` hierarchy from a number: box as an i31 first,
    // because the conversion takes a reference.
    (I32, Value(Ref({ typ: Extern, .. }))) => {
      out.push(RefI31)
      Ref({ nullable: false, typ: I31 })
    }
    (I64, Value(Ref({ typ: Extern, .. }))) => {
      out.push(I32WrapI64)
      out.push(RefI31)
      Ref({ nullable: false, typ: I31 })
    }
    // `extern as &T` for an `any`-hierarchy T: convert to `any` first, and let
    // the match below do the `ref.cast` to T. A non-null `&any` target from a
    // NULLABLE operand needs that cast too, to null-check the conversion's
    // result; a non-null operand already yields a non-null `any`, so it does
    // not, and falls to the convert alone.
    (
      Ref({ typ: Extern | NoExtern, .. }),
      Value(
        Ref({ typ: Eq | I31 | Struct | Array | Type(_) | Exact(_) | None_, .. })
      ),
    )
    | (
      Ref({ typ: Extern | NoExtern, nullable: true }),
      Value(Ref({ typ: Any, nullable: false })),
    ) => {
      out.push(AnyConvertExtern)
      Ref({ nullable: true, typ: Any })
    }
    _ => from
  }
}

///|
/// The `i31.get` of this signedness. An i31 is 31 bits wide, so reading it as
/// an i32 has to say what the missing bit becomes.
fn i31_get(signage : @wasm_types.Signage) -> @wasm_bin.Instruction {
  match signage {
    Signed => I31GetS
    Unsigned => I31GetU
  }
}

///|
/// Emit a cast's own instruction, given the type the prologue left.
///
/// Every arm here is one instruction or none. An identity cast is real in the
/// source -- it pins a width the checker used -- and nothing at all in the
/// output, which is why the reference calls its sentinel `Nop` and then elides
/// it.
fn Lowering::cast_instruction(
  self : Lowering,
  out : Array[@wasm_bin.Instruction],
  from : @wasm_types.ValType[Int],
  target : @ast.CastType,
  node : @ast.Instr[@typing_env.InferredAnnotation],
  loc : @basic.Location,
) -> Unit raise LowerError {
  match (from, target) {
    (I32, Value(Ref({ typ: I31, .. }))) => out.push(RefI31)
    (Ref(_), Signed(typ=I32, signage~, ..)) => out.push(i31_get(signage))
    (
      Ref(
        {
          typ: Any
          | Eq
          | I31
          | Struct
          | Array
          | Type(_)
          | Exact(_)
          | None_,
          ..,
        }
      ),
      Value(Ref({ typ: Extern, .. })),
    ) => out.push(ExternConvertAny)
    (Ref({ typ: Extern | NoExtern, .. }), Value(Ref({ typ: Any, .. }))) =>
      out.push(AnyConvertExtern)
    (Ref(_), Value(Ref(r))) => out.push(RefCast(self.reftype_index(r, loc)))
    (I64, Value(I32)) => out.push(I32WrapI64)
    (F64, Value(F32)) => out.push(F32DemoteF64)
    (F32, Value(F64)) => out.push(F64PromoteF32)
    (I32, Signed(typ=I64, signage~, ..)) =>
      out.push(
        match signage {
          Signed => I64ExtendI32S
          Unsigned => I64ExtendI32U
        },
      )
    // A cast to an inline function type is a `ref.cast` to the anonymous type
    // minted for the cast's own result.
    (_, Func(..)) => {
      guard node_valtype(node) is Some(Ref(r)) else {
        raise Unresolved("function-type cast", loc)
      }
      out.push(RefCast(r))
    }
    (_, Signed(typ~, signage~, strict~)) => {
      guard signed_cast(from, typ, signage, strict) is Some(instrs) else {
        raise NotLowered("signed cast between these types", loc)
      }
      for x in instrs {
        out.push(x)
      }
    }
    (_, Value(_)) => {
      guard node_valtype(node) is Some(to) else {
        raise Unresolved("cast target type", loc)
      }
      guard numeric_cast(from, to) is Some(instrs) else {
        raise NotLowered("cast between these types", loc)
      }
      for x in instrs {
        out.push(x)
      }
    }
  }
}