Add semantic functions for 32bit Zfhmin.

Full half precision support will be handled in follow up changes.

PiperOrigin-RevId: 752424597
Change-Id: I99e94b9b656a1ac1da6cee60b01f650969d72a0e
diff --git a/riscv/BUILD b/riscv/BUILD
index 95fea11..4983ea6 100644
--- a/riscv/BUILD
+++ b/riscv/BUILD
@@ -327,6 +327,49 @@
     ],
 )
 
+cc_library(
+    name = "riscv_zfh_instructions",
+    srcs = select({
+        "//third_party/bazel_platforms/cpu:aarch64": [
+            "riscv_zfh_instructions.cc",
+            "riscv_zfh_instructions_arm.cc",
+        ],
+        "//conditions:default": [
+            "riscv_zfh_instructions.cc",
+            "riscv_zfh_instructions_x86.cc",
+        ],
+    }),
+    hdrs = [
+        "riscv_instruction_helpers.h",
+        "riscv_zfh_instructions.h",
+    ],
+    copts = select({
+        "//third_party/bazel_platforms/cpu:aarch64": [
+            "-O3",
+            "-ffp-model=strict",
+        ],
+        "//buildenv/platforms/settings:macos_aarch64": [
+            "-O3",
+            "-ffp-model=strict",
+        ],
+        "//conditions:default": [
+            "-ffp-model=strict",
+            "-O3",
+            "-mf16c",
+        ],
+    }),
+    deps = [
+        ":riscv_fp_state",
+        ":riscv_state",
+        "@com_google_absl//absl/base",
+        "@com_google_absl//absl/log",
+        "@com_google_mpact-sim//mpact/sim/generic:arch_state",
+        "@com_google_mpact-sim//mpact/sim/generic:core",
+        "@com_google_mpact-sim//mpact/sim/generic:instruction",
+        "@com_google_mpact-sim//mpact/sim/generic:type_helpers",
+    ],
+)
+
 mpact_isa_decoder(
     name = "riscv32g_isa",
     src = "riscv32g.isa",
diff --git a/riscv/riscv_zfh.isa b/riscv/riscv_zfh.isa
new file mode 100644
index 0000000..6ba21c6
--- /dev/null
+++ b/riscv/riscv_zfh.isa
@@ -0,0 +1,86 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file contains the ISA description of the RiscV ZFH extention
+// instructions.
+
+isa ZFH {
+  namespace mpact::sim::riscv::zfh;
+  slots {
+    riscv32_zfh_min;
+  }
+}
+
+// First disasm field is 18 char wide and left justified.
+disasm widths = {-18};
+
+slot riscv_zfh_min {
+  includes {
+    #include "riscv/riscv_zfh_instructions.h"
+  }
+  default size = 4;
+  default latency = 0;
+  default opcode =
+    disasm: "Unimplemented instruction at 0x%(@:08x)",
+    semfunc: "&RV32VUnimplementedInstruction";
+  opcodes {
+    fmv_hx{: rs1 : frd},
+      resources: {next_pc, rs1 : frd[0..]},
+      semfunc: "&RiscVZfhFMvhx",
+      disasm: "fmv.h.x", "%frd, %rs1";
+    fcvt_sh{: frs1, rm : frd, fflags},
+      resources: {next_pc, frs1 : frd[0..]},
+      semfunc: "&RiscVZfhCvtSh",
+      disasm: "fcvt.s.h", "%frd, %frs1";
+    fcvt_hs{: frs1, rm : frd, fflags},
+      resources: {next_pc, frs1 : frd[0..]},
+      semfunc: "&RiscVZfhCvtHs",
+      disasm: "fcvt.h.s", "%frd, %frs1";
+    fcvt_dh{: frs1, rm : frd, fflags},
+      resources: {next_pc, frs1 : frd[0..]},
+      semfunc: "&RiscVZfhCvtDh",
+      disasm: "fcvt.d.h", "%frd, %frs1";
+    fcvt_hd{: frs1, rm : frd, fflags},
+      resources: {next_pc, frs1 : frd[0..]},
+      semfunc: "&RiscVZfhCvtHd",
+      disasm: "fcvt.h.d", "%frd, %frs1";
+  }
+}
+
+slot riscv32_zfh_min: riscv_zfh_min {
+  includes {
+    #include "riscv/riscv_i_instructions.h"
+    #include "riscv/riscv_zfh_instructions.h"
+  }
+  default size = 4;
+  default latency = 0;
+  default opcode =
+    disasm: "Unimplemented instruction at 0x%(@:08x)",
+    semfunc: "&RV32VUnimplementedInstruction";
+  opcodes {
+    flh{(: rs1, I_imm12 : ), (: : frd)},
+      resources: {next_pc, rs1 : frd[0..]},
+      semfunc: "&RV32::RiscVILhu", "&RiscVZfhFlhChild",
+      disasm: "flh", "%frd, %I_imm12(%rs1)";
+    fsh{: rs1, S_imm12, frs2},
+      resources: {next_pc, rs1, frs2},
+      semfunc: "&RV32::RiscVISh",
+      disasm: "fsh", "%frs2, %S_imm12(%rs1)";
+    fmv_xh{: frs1 : rd},
+      resources: {next_pc, frs1 : rd[0..]},
+      semfunc: "&RV32::RiscVZfhFMvxh",
+      disasm: "fmv.x.h", "%rd, %frs1";
+  }
+}
+
diff --git a/riscv/riscv_zfh_instructions.cc b/riscv/riscv_zfh_instructions.cc
new file mode 100644
index 0000000..34ce19d
--- /dev/null
+++ b/riscv/riscv_zfh_instructions.cc
@@ -0,0 +1,230 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "riscv/riscv_zfh_instructions.h"
+
+#include <cstdint>
+#include <functional>
+#include <limits>
+
+#include "absl/base/casts.h"
+#include "absl/log/log.h"
+#include "mpact/sim/generic/instruction.h"
+#include "mpact/sim/generic/register.h"
+#include "mpact/sim/generic/type_helpers.h"
+#include "riscv/riscv_csr.h"
+#include "riscv/riscv_fp_host.h"
+#include "riscv/riscv_fp_info.h"
+#include "riscv/riscv_instruction_helpers.h"
+#include "riscv/riscv_register.h"
+#include "riscv/riscv_state.h"
+
+namespace mpact {
+namespace sim {
+namespace riscv {
+
+using HalfFP = ::mpact::sim::generic::HalfFP;
+
+namespace {
+
+// Convert from half precision to single or double precision.
+template <typename T>
+inline T ConvertFromHalfFP(HalfFP half_fp, uint32_t &fflags) {
+  using UIntType = typename FPTypeInfo<T>::UIntType;
+  using HalfFPUIntType = typename FPTypeInfo<HalfFP>::UIntType;
+  HalfFPUIntType in_int = half_fp.value;
+
+  if (FPTypeInfo<HalfFP>::IsNaN(half_fp)) {
+    if (FPTypeInfo<HalfFP>::IsSNaN(half_fp)) {
+      fflags |= static_cast<uint32_t>(FPExceptions::kInvalidOp);
+    }
+    UIntType uint_value = FPTypeInfo<T>::kCanonicalNaN;
+    return absl::bit_cast<T>(uint_value);
+  }
+
+  if (FPTypeInfo<HalfFP>::IsInf(half_fp)) {
+    UIntType uint_value = FPTypeInfo<T>::kPosInf;
+    UIntType sign = in_int >> (FPTypeInfo<HalfFP>::kBitSize - 1);
+    uint_value |= sign << (FPTypeInfo<T>::kBitSize - 1);
+    return absl::bit_cast<T>(uint_value);
+  }
+
+  if (in_int == 0 || in_int == 1 << (FPTypeInfo<HalfFP>::kBitSize - 1)) {
+    UIntType uint_value =
+        static_cast<UIntType>(in_int)
+        << (FPTypeInfo<T>::kBitSize - FPTypeInfo<HalfFP>::kBitSize);
+    return absl::bit_cast<T>(uint_value);
+  }
+
+  UIntType in_sign = FPTypeInfo<HalfFP>::SignBit(half_fp);
+  UIntType in_exp =
+      (in_int & FPTypeInfo<HalfFP>::kExpMask) >> FPTypeInfo<HalfFP>::kSigSize;
+  UIntType in_sig = in_int & FPTypeInfo<HalfFP>::kSigMask;
+  UIntType out_int = 0;
+  UIntType out_sig = in_sig;
+  if (in_exp == 0 && in_sig != 0) {
+    // Handle subnormal half precision inputs. They always result in a normal
+    // float or double. Calculate how much shifting is needed move the MSB to
+    // the location of the implicit bit. Then it can be handled as a normal
+    // value from here on.
+    int32_t shift_count =
+        (1 + FPTypeInfo<HalfFP>::kSigSize) -
+        (std::numeric_limits<UIntType>::digits - absl::countl_zero(out_sig));
+    out_sig = (out_sig << shift_count) & FPTypeInfo<HalfFP>::kSigMask;
+    in_exp = 1 - shift_count;
+  }
+  out_int |= in_sign << (FPTypeInfo<T>::kBitSize - 1);
+  out_int |= (in_exp + FPTypeInfo<T>::kExpBias - FPTypeInfo<HalfFP>::kExpBias)
+             << FPTypeInfo<T>::kSigSize;
+  out_int |=
+      out_sig << (FPTypeInfo<T>::kSigSize - FPTypeInfo<HalfFP>::kSigSize);
+  return absl::bit_cast<T>(out_int);
+}
+
+template <typename Result, typename Argument>
+void RiscVZfhCvtHelper(
+    const Instruction *instruction,
+    std::function<Result(Argument, FPRoundingMode, uint32_t &)> operation) {
+  uint32_t fflags = 0;
+  RiscVFPState *fp_state =
+      static_cast<RiscVState *>(instruction->state())->rv_fp();
+  int rm_value = generic::GetInstructionSource<int>(instruction, 1);
+
+  // If the rounding mode is dynamic, read it from the current state.
+  if (rm_value == *FPRoundingMode::kDynamic) {
+    if (!fp_state->rounding_mode_valid()) {
+      LOG(ERROR) << "Invalid rounding mode";
+      return;
+    }
+    rm_value = *(fp_state->GetRoundingMode());
+  }
+  FPRoundingMode rm = static_cast<FPRoundingMode>(rm_value);
+  RiscVCsrDestinationOperand *fflags_dest =
+      static_cast<RiscVCsrDestinationOperand *>(instruction->Destination(1));
+  RiscVUnaryFloatNaNBoxOp<RVFpRegister::ValueType, RVFpRegister::ValueType,
+                          Result, Argument>(
+      instruction, [fp_state, rm, &fflags, &operation](Argument a) -> Result {
+        Result result;
+        if (zfh_internal::UseHostFlagsForConversion()) {
+          result = operation(a, rm, fflags);
+        } else {
+          ScopedFPStatus set_fpstatus(fp_state->host_fp_interface(), rm);
+          result = operation(a, rm, fflags);
+        }
+        return result;
+      });
+  if (!zfh_internal::UseHostFlagsForConversion()) {
+    fflags_dest->GetRiscVCsr()->SetBits(fflags);
+  }
+}
+
+}  // namespace
+
+namespace RV32 {
+
+// Move a half precision value from a float register to a 32 bit integer
+// register.
+void RiscVZfhFMvxh(const Instruction *instruction) {
+  RiscVUnaryFloatOp<uint32_t, HalfFP>(instruction, [](HalfFP a) -> uint32_t {
+    if (FPTypeInfo<HalfFP>::SignBit(a)) {
+      // Repeat the sign bit for negative values.
+      return 0xFFFF'0000 | a.value;
+    }
+    return static_cast<uint32_t>(a.value);
+  });
+}
+
+}  // namespace RV32
+
+void RiscVZfhFlhChild(const Instruction *instruction) {
+  using FPUInt = typename FPTypeInfo<HalfFP>::UIntType;
+  LoadContext *context = static_cast<LoadContext *>(instruction->context());
+  auto value = context->value_db->Get<FPUInt>(0);
+  auto *reg =
+      static_cast<
+          generic::RegisterDestinationOperand<RVFpRegister::ValueType> *>(
+          instruction->Destination(0))
+          ->GetRegister();
+  if (sizeof(RVFpRegister::ValueType) > sizeof(FPUInt)) {
+    // NaN box the loaded value.
+    auto reg_value = std::numeric_limits<RVFpRegister::ValueType>::max();
+    reg_value <<= sizeof(FPUInt) * 8;
+    reg_value |= value;
+    reg->data_buffer()->Set<RVFpRegister::ValueType>(0, reg_value);
+    return;
+  }
+  reg->data_buffer()->Set<RVFpRegister::ValueType>(0, value);
+}
+
+// Move a half precision value from an integer register to a float register.
+void RiscVZfhFMvhx(const Instruction *instruction) {
+  RiscVUnaryFloatOp<HalfFP, uint64_t>(instruction, [](uint64_t a) -> HalfFP {
+    return HalfFP{.value = static_cast<uint16_t>(a)};
+  });
+}
+
+// Convert from half precision to single precision.
+void RiscVZfhCvtSh(const Instruction *instruction) {
+  uint32_t fflags = 0;
+  RiscVCsrDestinationOperand *fflags_dest =
+      static_cast<RiscVCsrDestinationOperand *>(instruction->Destination(1));
+  RiscVUnaryFloatNaNBoxOp<RVFpRegister::ValueType, RVFpRegister::ValueType,
+                          float, HalfFP>(
+      instruction, [&fflags](HalfFP a) -> float {
+        return ConvertFromHalfFP<float>(a, fflags);
+      });
+  fflags_dest->GetRiscVCsr()->SetBits(fflags);
+}
+
+// Convert from single precision to half precision.
+void RiscVZfhCvtHs(const Instruction *instruction) {
+  RiscVZfhCvtHelper<HalfFP, float>(
+      instruction, [](float a, FPRoundingMode rm, uint32_t &fflags) -> HalfFP {
+        return ConvertSingleToHalfFP(a, rm, fflags);
+      });
+}
+
+// Convert from half precision to double precision.
+void RiscVZfhCvtDh(const Instruction *instruction) {
+  uint32_t fflags = 0;
+  RiscVCsrDestinationOperand *fflags_dest =
+      static_cast<RiscVCsrDestinationOperand *>(instruction->Destination(1));
+  RiscVUnaryFloatNaNBoxOp<RVFpRegister::ValueType, RVFpRegister::ValueType,
+                          double, HalfFP>(
+      instruction, [&fflags](HalfFP a) -> double {
+        return ConvertFromHalfFP<double>(a, fflags);
+      });
+  fflags_dest->GetRiscVCsr()->SetBits(fflags);
+}
+
+// Convert from double precision to half precision.
+void RiscVZfhCvtHd(const Instruction *instruction) {
+  RiscVZfhCvtHelper<HalfFP, double>(
+      instruction, [](double a, FPRoundingMode rm, uint32_t &fflags) -> HalfFP {
+        return ConvertDoubleToHalfFP(a, rm, fflags);
+      });
+}
+
+// TODO(b/409778536): Factor out generic unimplemented instruction semantic
+//                    function.
+void RV32VUnimplementedInstruction(const Instruction *instruction) {
+  auto *state = static_cast<RiscVState *>(instruction->state());
+  state->Trap(/*is_interrupt*/ false, /*trap_value*/ 0,
+              *ExceptionCode::kIllegalInstruction,
+              /*epc*/ instruction->address(), instruction);
+}
+
+}  // namespace riscv
+}  // namespace sim
+}  // namespace mpact
diff --git a/riscv/riscv_zfh_instructions.h b/riscv/riscv_zfh_instructions.h
new file mode 100644
index 0000000..536dbc3
--- /dev/null
+++ b/riscv/riscv_zfh_instructions.h
@@ -0,0 +1,57 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef THIRD_PARTY_MPACT_RISCV_RISCV_ZFH_INSTRUCTIONS_H_
+#define THIRD_PARTY_MPACT_RISCV_RISCV_ZFH_INSTRUCTIONS_H_
+
+#include <cstdint>
+
+#include "mpact/sim/generic/instruction.h"
+#include "mpact/sim/generic/type_helpers.h"
+#include "riscv/riscv_fp_info.h"
+
+namespace mpact {
+namespace sim {
+namespace riscv {
+
+using ::mpact::sim::generic::Instruction;
+using HalfFP = ::mpact::sim::generic::HalfFP;
+
+namespace RV32 {
+void RiscVZfhFMvxh(const Instruction *instruction);
+}  // namespace RV32
+
+namespace RV64 {}  // namespace RV64
+
+void RiscVZfhFlhChild(const Instruction *instruction);
+void RiscVZfhFMvhx(const Instruction *instruction);
+void RiscVZfhCvtSh(const Instruction *instruction);
+void RiscVZfhCvtHs(const Instruction *instruction);
+void RiscVZfhCvtDh(const Instruction *instruction);
+void RiscVZfhCvtHd(const Instruction *instruction);
+// TODO(b/409778536): Factor out generic unimplemented instruction semantic
+//                    function.
+void RV32VUnimplementedInstruction(const Instruction *instruction);
+HalfFP ConvertSingleToHalfFP(float, FPRoundingMode, uint32_t &);
+HalfFP ConvertDoubleToHalfFP(double, FPRoundingMode, uint32_t &);
+
+namespace zfh_internal {
+bool UseHostFlagsForConversion();
+}  // namespace zfh_internal
+
+}  // namespace riscv
+}  // namespace sim
+}  // namespace mpact
+
+#endif  // THIRD_PARTY_MPACT_RISCV_RISCV_ZFH_INSTRUCTIONS_H_
diff --git a/riscv/riscv_zfh_instructions_arm.cc b/riscv/riscv_zfh_instructions_arm.cc
new file mode 100644
index 0000000..c8353fd
--- /dev/null
+++ b/riscv/riscv_zfh_instructions_arm.cc
@@ -0,0 +1,220 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <sys/types.h>
+
+#include <cassert>
+#include <cmath>
+#include <cstdint>
+
+#include "absl/base/casts.h"
+#include "mpact/sim/generic/type_helpers.h"
+#include "riscv/riscv_fp_info.h"
+#include "riscv/riscv_instruction_helpers.h"
+#include "riscv/riscv_zfh_instructions.h"
+
+namespace mpact {
+namespace sim {
+namespace riscv {
+
+using HalfFP = ::mpact::sim::generic::HalfFP;
+
+// TODO(b/401856759): Use arm intrinsics for fp32 -> fp16 and fp64 -> fp16
+//                    conversions.
+
+namespace {
+
+// This is a soft conversion from a float or double to a half precision value.
+// It is not a direct conversion from the floating point format to the half
+// format. Instead, it uses the floating point hardware to do the conversion.
+// This is done to get the correct rounding behavior for free from the FPU.
+template <typename T>
+HalfFP SoftConvertToHalfFP(T input_value, FPRoundingMode rm, uint32_t &fflags) {
+  using UIntType = typename FPTypeInfo<T>::UIntType;
+  using IntType = typename FPTypeInfo<T>::IntType;
+  UIntType in_int = absl::bit_cast<UIntType>(input_value);
+  HalfFP half_fp = {.value = 0x0000};
+
+  // Extract the mantissa, exponent and sign.
+  UIntType mantissa = in_int & FPTypeInfo<T>::kSigMask;
+  UIntType exponent =
+      (in_int & FPTypeInfo<T>::kExpMask) >> FPTypeInfo<T>::kSigSize;
+  UIntType sign = in_int >> (FPTypeInfo<T>::kBitSize - 1);
+
+  if (std::isnan(input_value)) {
+    half_fp.value = FPTypeInfo<HalfFP>::kCanonicalNaN;
+    if (FPTypeInfo<T>::IsSNaN(input_value)) {
+      fflags |= static_cast<UIntType>(FPExceptions::kInvalidOp);
+    }
+    return half_fp;
+  }
+
+  if (std::isinf(input_value)) {
+    half_fp.value = FPTypeInfo<HalfFP>::kPosInf;
+    half_fp.value |= (sign & 1) << (FPTypeInfo<HalfFP>::kBitSize - 1);
+    return half_fp;
+  }
+
+  if (in_int == 0 || in_int == 1ULL << (FPTypeInfo<T>::kBitSize - 1)) {
+    half_fp.value =
+        in_int >> (FPTypeInfo<T>::kBitSize - FPTypeInfo<HalfFP>::kBitSize);
+    return half_fp;
+  }
+
+  IntType bias_diff = FPTypeInfo<T>::kExpBias - FPTypeInfo<HalfFP>::kExpBias;
+  IntType unbounded_half_exponent = static_cast<IntType>(exponent) - bias_diff;
+  IntType sig_size_diff =
+      FPTypeInfo<T>::kSigSize - FPTypeInfo<HalfFP>::kSigSize;
+  UIntType half_inf_exponent = ((1 << FPTypeInfo<HalfFP>::kExpSize) - 1);
+  UIntType source_type_inf_exponent = ((1 << FPTypeInfo<T>::kExpSize) - 1);
+
+  // Create a temp float with the smallest normal exponent and input mantissa.
+  T ftmp = absl::bit_cast<T>(
+      (sign << (FPTypeInfo<T>::kBitSize - 1)) |
+      (static_cast<UIntType>(1ULL) << FPTypeInfo<T>::kSigSize) | mantissa);
+
+  // Create a divisor float that will be used for shifting the mantissa in a
+  // rounding aware way. The amount of shifting depends on if the result is
+  // subnormal or normal.
+  T fdiv = 0;
+  UIntType default_fdiv_exp = FPTypeInfo<T>::kExpBias + sig_size_diff;
+  UIntType fdiv_exp = default_fdiv_exp;
+  if (unbounded_half_exponent > 0) {
+    fdiv_exp = default_fdiv_exp;
+  } else if (unbounded_half_exponent < 0) {
+    // shift_count: emin - unbiased exponent
+    IntType shift_count = 1 - static_cast<int>(exponent) + bias_diff;
+    fdiv_exp = default_fdiv_exp + shift_count;
+    fdiv_exp = std::min(fdiv_exp, source_type_inf_exponent - 1);
+  } else {
+    fdiv_exp = default_fdiv_exp + 1;
+  }
+  fdiv = absl::bit_cast<T>(fdiv_exp << FPTypeInfo<T>::kSigSize);
+
+  // Shift right by doing division.
+  T fres = ftmp / fdiv;
+  UIntType res = absl::bit_cast<UIntType>(fres);
+
+  // Shift left by doing multiplication.
+  T fmultiply = absl::bit_cast<T>(default_fdiv_exp << FPTypeInfo<T>::kSigSize);
+  T fres2 = fres * fmultiply;
+  UIntType res2 = absl::bit_cast<UIntType>(fres2);
+
+  // Update the exponent if rounding caused an increase.
+  IntType exp_diff = static_cast<IntType>((res2 >> FPTypeInfo<T>::kSigSize) &
+                                          source_type_inf_exponent) -
+                     1;
+  UIntType new_exponent = (exponent + exp_diff) & source_type_inf_exponent;
+
+  UIntType half_exponent = 0;
+  if (unbounded_half_exponent > 0) {
+    half_exponent = new_exponent - bias_diff;
+  } else if (unbounded_half_exponent < 0) {
+    // Guaranteed subnormal. Nothing to do.
+  } else {
+    // This case could be normal or subnormal depending on the rounding result.
+    half_exponent = (res2 >> FPTypeInfo<T>::kSigSize) & half_inf_exponent;
+  }
+
+  UIntType half_mantissa =
+      (res2 >> sig_size_diff) & FPTypeInfo<HalfFP>::kSigMask;
+  if (unbounded_half_exponent < 0) {  // Guaranteed Subnormal
+    half_mantissa = (res & (1 << FPTypeInfo<HalfFP>::kSigSize))
+                        ? ((res >> 1) & FPTypeInfo<HalfFP>::kSigMask)
+                        : res & FPTypeInfo<HalfFP>::kSigMask;
+  }
+
+  // Handle the rules for overflowing to infinity depending on the rounding
+  // mode.
+  if (half_exponent >= half_inf_exponent) {
+    fflags |= static_cast<uint32_t>(FPExceptions::kOverflow);
+    fflags |= static_cast<uint32_t>(FPExceptions::kInexact);
+    switch (rm) {
+      case FPRoundingMode::kRoundToNearest:
+        half_exponent = half_inf_exponent;
+        half_mantissa = 0;
+        break;
+      case FPRoundingMode::kRoundTowardsZero:
+        half_exponent = half_inf_exponent - 1;
+        half_mantissa = FPTypeInfo<HalfFP>::kSigMask;
+        break;
+      case FPRoundingMode::kRoundDown:
+        half_exponent = sign ? half_inf_exponent : half_inf_exponent - 1;
+        half_mantissa = sign ? 0 : FPTypeInfo<HalfFP>::kSigMask;
+        break;
+      case FPRoundingMode::kRoundUp:
+        half_exponent = sign ? half_inf_exponent - 1 : half_inf_exponent;
+        half_mantissa = sign ? FPTypeInfo<HalfFP>::kSigMask : 0;
+        break;
+      default:
+        half_exponent = half_inf_exponent;
+        half_mantissa = 0;
+        break;
+    }
+  }
+
+  // Handle flags for the specific underflow case.
+  if (unbounded_half_exponent < 0 ||
+      (unbounded_half_exponent == 0 && fres2 != ftmp)) {
+    fflags |= static_cast<uint32_t>(FPExceptions::kUnderflow);
+  }
+
+  // Handle flags for the specific inexact case.
+  if (fres2 != ftmp) {
+    fflags |= static_cast<uint32_t>(FPExceptions::kInexact);
+  }
+
+  // Construct the half float.
+  half_fp.value = half_mantissa |
+                  (half_exponent << FPTypeInfo<HalfFP>::kSigSize) |
+                  (sign << (FPTypeInfo<HalfFP>::kBitSize - 1));
+
+  // Do an arithmetic reconstruction of the float to check for exactness.
+  T trailing_significand_float = static_cast<T>(half_mantissa);
+  T precision_factor = std::pow(2.0, -1.0 * FPTypeInfo<HalfFP>::kSigSize);
+  IntType unbiased_exponent =
+      (half_exponent == 0 ? 1 : half_exponent) - FPTypeInfo<HalfFP>::kExpBias;
+  T exponent_factor = std::pow(2.0, unbiased_exponent);
+  T sign_factor = sign == 1 ? -1.0 : 1.0;
+  T implicit_bit_adjustment = half_exponent == 0 ? 0.0 : 1.0;
+  T reconstructed_value = ((trailing_significand_float * precision_factor) +
+                           implicit_bit_adjustment) *
+                          exponent_factor * sign_factor;
+
+  if (reconstructed_value == input_value) {
+    // Clear the flags for exact conversions.
+    fflags &= ~(static_cast<uint32_t>(FPExceptions::kUnderflow) |
+                static_cast<uint32_t>(FPExceptions::kInexact));
+  }
+  return half_fp;
+}
+}  // namespace
+
+HalfFP ConvertSingleToHalfFP(float input_value, FPRoundingMode rm,
+                             uint32_t &fflags) {
+  return SoftConvertToHalfFP(input_value, rm, fflags);
+}
+
+HalfFP ConvertDoubleToHalfFP(double input_value, FPRoundingMode rm,
+                             uint32_t &fflags) {
+  return SoftConvertToHalfFP(input_value, rm, fflags);
+}
+
+namespace zfh_internal {
+bool UseHostFlagsForConversion() { return false; }
+}  // namespace zfh_internal
+
+}  // namespace riscv
+}  // namespace sim
+}  // namespace mpact
diff --git a/riscv/riscv_zfh_instructions_x86.cc b/riscv/riscv_zfh_instructions_x86.cc
new file mode 100644
index 0000000..f99a0fa
--- /dev/null
+++ b/riscv/riscv_zfh_instructions_x86.cc
@@ -0,0 +1,74 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <immintrin.h>
+#include <sys/types.h>
+
+#include <cstdint>
+
+#include "mpact/sim/generic/type_helpers.h"
+#include "riscv/riscv_fp_info.h"
+#include "riscv/riscv_zfh_instructions.h"
+
+namespace mpact {
+namespace sim {
+namespace riscv {
+
+using HalfFP = ::mpact::sim::generic::HalfFP;
+
+HalfFP ConvertSingleToHalfFP(float input_value, FPRoundingMode rm,
+                             uint32_t &fflags) {
+  HalfFP half_fp;
+
+  // Get current MXCSR value. The simulator should have already configured the
+  // rounding mode so we simply pass it along to the intrinsic.
+  unsigned int mxcsr = _mm_getcsr();
+
+  // Extract rounding control bits (bits 13 and 14)
+  int rounding_control_bits = (mxcsr >> 13) & 0x3;
+
+  switch (rounding_control_bits) {
+    case 0x0:  // Round to nearest
+      half_fp.value = _cvtss_sh(input_value, 0);
+      break;
+    case 0x1:  // Round down
+      half_fp.value = _cvtss_sh(input_value, 1);
+      break;
+    case 0x2:  // Round up
+      half_fp.value = _cvtss_sh(input_value, 2);
+      break;
+    case 0x3:  // Round towards zero
+      half_fp.value = _cvtss_sh(input_value, 3);
+      break;
+    default:  // Default to nearest even if mode is not recognized
+      half_fp.value = _cvtss_sh(input_value, 0);
+      break;
+  }
+
+  return half_fp;
+}
+
+HalfFP ConvertDoubleToHalfFP(double input_value, FPRoundingMode rm,
+                             uint32_t &fflags) {
+  float input_float = static_cast<float>(input_value);
+  return ConvertSingleToHalfFP(input_float, rm, fflags);
+}
+
+namespace zfh_internal {
+bool UseHostFlagsForConversion() { return true; }
+}  // namespace zfh_internal
+
+}  // namespace riscv
+}  // namespace sim
+}  // namespace mpact
diff --git a/riscv/test/BUILD b/riscv/test/BUILD
index 4332d87..da3f7e6 100644
--- a/riscv/test/BUILD
+++ b/riscv/test/BUILD
@@ -57,6 +57,7 @@
     deps = [
         "//riscv:riscv_fp_state",
         "//riscv:riscv_state",
+        "@com_google_absl//absl/base",
         "@com_google_absl//absl/log",
         "@com_google_absl//absl/random",
         "@com_google_absl//absl/strings",
@@ -207,6 +208,32 @@
 )
 
 cc_test(
+    name = "riscv_zfh_instructions_test",
+    size = "small",
+    srcs = ["riscv_zfh_instructions_test.cc"],
+    copts = select({
+        "darwin_arm64_cpu": ["-ffp-model=strict"],
+        "//conditions:default": [
+            "-ffp-model=strict",
+            "-fprotect-parens",
+        ],
+    }),
+    tags = ["not_run:arm"],
+    deps = [
+        ":riscv_fp_test_base",
+        "//riscv:riscv_fp_state",
+        "//riscv:riscv_g",
+        "//riscv:riscv_state",
+        "//riscv:riscv_zfh_instructions",
+        "@com_google_absl//absl/base",
+        "@com_google_googletest//:gtest_main",
+        "@com_google_mpact-sim//mpact/sim/generic:core",
+        "@com_google_mpact-sim//mpact/sim/generic:instruction",
+        "@com_google_mpact-sim//mpact/sim/generic:type_helpers",
+    ],
+)
+
+cc_test(
     name = "riscv32g_encoding_test",
     size = "small",
     srcs = [
diff --git a/riscv/test/riscv_fp_test_base.h b/riscv/test/riscv_fp_test_base.h
index 81c8b07..f60d156 100644
--- a/riscv/test/riscv_fp_test_base.h
+++ b/riscv/test/riscv_fp_test_base.h
@@ -15,6 +15,10 @@
 #ifndef MPACT_RISCV_RISCV_TEST_RISCV_FP_TEST_BASE_H_
 #define MPACT_RISCV_RISCV_TEST_RISCV_FP_TEST_BASE_H_
 
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <cassert>
 #include <cmath>
 #include <cstdint>
 #include <functional>
@@ -25,6 +29,7 @@
 #include <type_traits>
 #include <vector>
 
+#include "absl/base/casts.h"
 #include "absl/log/log.h"
 #include "absl/random/random.h"
 #include "absl/strings/str_cat.h"
@@ -48,9 +53,8 @@
 using ::mpact::sim::generic::ConvertHalfToSingle;
 using ::mpact::sim::generic::FloatingPointToString;
 using ::mpact::sim::generic::HalfFP;
-using ::mpact::sim::generic::IsMpactFp;
-
 using ::mpact::sim::generic::Instruction;
+using ::mpact::sim::generic::IsMpactFp;
 using ::mpact::sim::riscv::FPRoundingMode;
 using ::mpact::sim::util::FlatDemandMemory;
 
@@ -100,7 +104,7 @@
   static const IntType kCanonicalNaN = 0x7fc0'0000ULL;
   static bool IsNaN(T value) { return std::isnan(value); }
   static bool IsQNaN(T value) {
-    IntType uint_val = *reinterpret_cast<IntType *>(&value);
+    IntType uint_val = absl::bit_cast<IntType>(value);
     return IsNaN(value) && (((1ULL << (kSigSize - 1)) & uint_val) != 0);
   }
   static bool IsInf(T value) { return std::isinf(value); }
@@ -128,7 +132,7 @@
   static const IntType kCanonicalNaN = 0x7ff8'0000'0000'0000ULL;
   static bool IsNaN(T value) { return std::isnan(value); }
   static bool IsQNaN(T value) {
-    IntType uint_val = *reinterpret_cast<IntType *>(&value);
+    IntType uint_val = absl::bit_cast<IntType>(value);
     return IsNaN(value) && (((1ULL << (kSigSize - 1)) & uint_val) != 0);
   }
   static bool IsInf(T value) { return std::isinf(value); }
@@ -161,7 +165,7 @@
     return (exp == (1 << kExpSize) - 1) && (sig != 0);
   }
   static bool IsQNaN(T value) {
-    IntType uint_val = *reinterpret_cast<IntType *>(&value);
+    IntType uint_val = absl::bit_cast<IntType>(value);
     IntType significand_msb = (uint_val >> (kSigSize - 1)) & 1;
     return IsNaN(value) && (significand_msb != 0);
   }
@@ -181,8 +185,7 @@
     case FP_INFINITE:
       return std::signbit(val) ? 1 : 1 << 7;
     case FP_NAN: {
-      auto uint_val =
-          *reinterpret_cast<typename FPTypeInfo<T>::IntType *>(&val);
+      auto uint_val = absl::bit_cast<typename FPTypeInfo<T>::IntType>(val);
       bool quiet_nan = (uint_val >> (FPTypeInfo<T>::kSigSize - 1)) & 1;
       return quiet_nan ? 1 << 9 : 1 << 8;
     }
@@ -210,8 +213,8 @@
                              absl::string_view str) {
   using T = float;
   using UInt = typename FPTypeInfo<T>::IntType;
-  UInt u_op = *reinterpret_cast<UInt *>(&op);
-  UInt u_reg = *reinterpret_cast<UInt *>(&reg);
+  UInt u_op = absl::bit_cast<UInt>(op);
+  UInt u_reg = absl::bit_cast<UInt>(reg);
   if (!std::isnan(op) && !std::isinf(op) &&
       delta_position < FPTypeInfo<T>::kSigSize) {
     T delta;
@@ -219,12 +222,12 @@
     if (exp > delta_position) {
       exp -= delta_position;
       UInt udelta = exp << FPTypeInfo<T>::kSigSize;
-      delta = *reinterpret_cast<T *>(&udelta);
+      delta = absl::bit_cast<T>(udelta);
     } else {
       // Becomes a denormal
       int diff = delta_position - exp;
       UInt udelta = 1ULL << (FPTypeInfo<T>::kSigSize - 1 - diff);
-      delta = *reinterpret_cast<T *>(&udelta);
+      delta = absl::bit_cast<T>(udelta);
     }
     EXPECT_THAT(reg, testing::NanSensitiveFloatNear(op, delta))
         << str << "  op: " << std::hex << u_op << "  reg: " << std::hex
@@ -241,8 +244,8 @@
                               absl::string_view str) {
   using T = double;
   using UInt = typename FPTypeInfo<T>::IntType;
-  UInt u_op = *reinterpret_cast<UInt *>(&op);
-  UInt u_reg = *reinterpret_cast<UInt *>(&reg);
+  UInt u_op = absl::bit_cast<UInt>(op);
+  UInt u_reg = absl::bit_cast<UInt>(reg);
   if (!std::isnan(op) && !std::isinf(op) &&
       delta_position < FPTypeInfo<T>::kSigSize) {
     T delta;
@@ -250,12 +253,12 @@
     if (exp > delta_position) {
       exp -= delta_position;
       UInt udelta = exp << FPTypeInfo<T>::kSigSize;
-      delta = *reinterpret_cast<T *>(&udelta);
+      delta = absl::bit_cast<T>(udelta);
     } else {
       // Becomes a denormal
       int diff = delta_position - exp;
       UInt udelta = 1ULL << (FPTypeInfo<T>::kSigSize - 1 - diff);
-      delta = *reinterpret_cast<T *>(&udelta);
+      delta = absl::bit_cast<T>(udelta);
     }
     EXPECT_THAT(reg, testing::NanSensitiveDoubleNear(op, delta))
         << str << "  op: " << std::hex << u_op << "  reg: " << std::hex
@@ -320,7 +323,7 @@
   using SInt = typename FPTypeInfo<S>::IntType;
   SInt sval = absl::bit_cast<SInt>(value);
   D dval = (~static_cast<D>(0) << (sizeof(S) * 8)) | sval;
-  return *reinterpret_cast<D *>(&dval);
+  return absl::bit_cast<D>(dval);
 }
 
 // This version does a straight copy - as the data types are the same size.
@@ -466,8 +469,8 @@
                              1ULL << FPTypeInfo<T>::kSigSize);
     UInt value = (sign & 1) << (FPTypeInfo<T>::kBitSize - 1) |
                  (exp << FPTypeInfo<T>::kSigSize) | sig;
-    T val = *reinterpret_cast<T *>(&value);
-    return val;
+    return absl::bit_cast<T>(value);
+    ;
   }
 
   // This method uses random values for each field in the fp number.
@@ -523,7 +526,8 @@
       for (int rm : {0, 1, 2, 3, 4}) {
         rv_fp_->SetRoundingMode(static_cast<FPRoundingMode>(rm));
         SetRegisterValues<int, RV32Register>({{kRmName, rm}});
-        SetRegisterValues<R, DestRegisterType>({{kRdName, 0}});
+        SetRegisterValues<DestRegisterType::ValueType, DestRegisterType>(
+            {{kRdName, 0}});
 
         inst->Execute(nullptr);
 
@@ -592,7 +596,8 @@
         rv_fp_->SetRoundingMode(static_cast<FPRoundingMode>(rm));
         rv_fp_->fflags()->Write(static_cast<uint32_t>(0));
         SetRegisterValues<int, RV32Register>({{kRmName, rm}, {}});
-        SetRegisterValues<R, DestRegisterType>({{kRdName, 0}});
+        SetRegisterValues<DestRegisterType::ValueType, DestRegisterType>(
+            {{kRdName, 0}});
 
         inst->Execute(nullptr);
         auto instruction_fflags = rv_fp_->fflags()->GetUint32();
@@ -611,8 +616,8 @@
             op_val, reg_val, delta_position,
             absl::StrCat(name, "  ", i, ": ",
                          FloatingPointToString<LHS>(lhs_span[i]), " rm: ", rm));
-        auto lhs_uint = *reinterpret_cast<LhsInt *>(&lhs_span[i]);
-        auto op_val_uint = *reinterpret_cast<RInt *>(&op_val);
+        LhsInt lhs_uint = absl::bit_cast<LhsInt>(lhs_span[i]);
+        RInt op_val_uint = absl::bit_cast<RInt>(op_val);
         EXPECT_EQ(test_operation_fflags, instruction_fflags)
             << name << "(" << FloatingPointToString<LHS>(lhs_span[i]) << ")  "
             << std::hex << name << "(0x" << lhs_uint
@@ -776,8 +781,8 @@
                      absl::StrCat(name, "  ", i, ": ",
                                   FloatingPointToString<LHS>(lhs_span[i]), "  ",
                                   FloatingPointToString<RHS>(rhs_span[i])));
-        auto lhs_uint = *reinterpret_cast<LhsUInt *>(&lhs_span[i]);
-        auto rhs_uint = *reinterpret_cast<RhsUInt *>(&rhs_span[i]);
+        LhsUInt lhs_uint = absl::bit_cast<LhsUInt>(lhs_span[i]);
+        RhsUInt rhs_uint = absl::bit_cast<RhsUInt>(rhs_span[i]);
         EXPECT_EQ(test_operation_fflags, instruction_fflags)
             << std::hex << name << "(" << lhs_uint << ", " << rhs_uint << ")";
       }
@@ -992,7 +997,7 @@
       auto constexpr kSigSize = FPTypeInfo<From>::kSigSize;
       auto constexpr kSigMask = FPTypeInfo<From>::kSigMask;
       auto constexpr kBitSize = FPTypeInfo<From>::kBitSize;
-      FromUint val_u = *reinterpret_cast<FromUint *>(&val);
+      FromUint val_u = absl::bit_cast<FromUint>(val);
       FromUint exp = kExpMask & val_u;
       const bool sign = (val_u & (1ULL << (kBitSize - 1))) != 0;
       int exp_value = exp >> kSigSize;
@@ -1152,6 +1157,485 @@
   absl::BitGen bitgen_;
 };
 
+namespace internal {
+
+template <typename T>
+struct UnsignedToFpType {};
+
+template <>
+struct UnsignedToFpType<uint16_t> {
+  using FpType = HalfFP;
+};
+
+template <>
+struct UnsignedToFpType<uint32_t> {
+  using FpType = float;
+};
+
+template <>
+struct UnsignedToFpType<uint64_t> {
+  using FpType = double;
+};
+
+template <typename T>
+double ToDouble(T input) {
+  using IntType = typename FPTypeInfo<T>::IntType;
+  using FpType = typename internal::UnsignedToFpType<IntType>::FpType;
+
+  IntType uint_val = absl::bit_cast<IntType>(input);
+  FpType fp_val = absl::bit_cast<FpType>(uint_val);
+  if (FPTypeInfo<FpType>::IsNaN(fp_val)) {
+    return std::numeric_limits<double>::quiet_NaN();
+  } else if (FPTypeInfo<FpType>::IsInf(fp_val)) {
+    return std::numeric_limits<double>::infinity();
+  }
+  IntType exp =
+      (uint_val & FPTypeInfo<FpType>::kExpMask) >> FPTypeInfo<FpType>::kSigSize;
+  IntType sig = uint_val & FPTypeInfo<FpType>::kSigMask;
+  int32_t unbiased_exponent =
+      exp ? static_cast<int32_t>(exp) - FPTypeInfo<FpType>::kExpBias
+          : 1 - static_cast<int32_t>(FPTypeInfo<FpType>::kExpBias);
+
+  double exponent_factor = std::pow(2.0, unbiased_exponent);
+  double significand_factor = static_cast<double>(sig);
+  double precision_factor =
+      std::pow(2.0, -static_cast<int32_t>(FPTypeInfo<FpType>::kSigSize));
+  double implicit_bit_adjustment = exp ? 1.0 : 0.0;
+  double sign_factor =
+      std::pow(-1.0, uint_val >> (FPTypeInfo<FpType>::kBitSize - 1));
+  return ((significand_factor * precision_factor) + implicit_bit_adjustment) *
+         exponent_factor * sign_factor;
+}
+
+// sign, lsb, guard, round, sticky --- rm = 0
+inline constexpr int kRoundToNearestTable[] = {
+    0, /*00000*/
+    0, /*00001*/
+    0, /*00010*/
+    0, /*00011*/
+    0, /*00100*/
+    1, /*00101*/
+    1, /*00110*/
+    1, /*00111*/
+    0, /*01000*/
+    0, /*01001*/
+    0, /*01010*/
+    0, /*01011*/
+    1, /*01100*/
+    1, /*01101*/
+    1, /*01110*/
+    1, /*01111*/
+    0, /*10000*/
+    0, /*10001*/
+    0, /*10010*/
+    0, /*10011*/
+    0, /*10100*/
+    1, /*10101*/
+    1, /*10110*/
+    1, /*10111*/
+    0, /*11000*/
+    0, /*11001*/
+    0, /*11010*/
+    0, /*11011*/
+    1, /*11100*/
+    1, /*11101*/
+    1, /*11110*/
+    1, /*11111*/
+};
+// sign, lsb, guard, round, sticky --- rm = 1
+inline constexpr int kRoundTowardsZeroTable[] = {
+    0, /*00000*/
+    0, /*00001*/
+    0, /*00010*/
+    0, /*00011*/
+    0, /*00100*/
+    0, /*00101*/
+    0, /*00110*/
+    0, /*00111*/
+    0, /*01000*/
+    0, /*01001*/
+    0, /*01010*/
+    0, /*01011*/
+    0, /*01100*/
+    0, /*01101*/
+    0, /*01110*/
+    0, /*01111*/
+    0, /*10000*/
+    0, /*10001*/
+    0, /*10010*/
+    0, /*10011*/
+    0, /*10100*/
+    0, /*10101*/
+    0, /*10110*/
+    0, /*10111*/
+    0, /*11000*/
+    0, /*11001*/
+    0, /*11010*/
+    0, /*11011*/
+    0, /*11100*/
+    0, /*11101*/
+    0, /*11110*/
+    0, /*11111*/
+};
+// sign, lsb, guard, round, sticky --- rm = 2
+inline constexpr int kRoundDownTable[] = {
+    0, /*00000*/
+    0, /*00001*/
+    0, /*00010*/
+    0, /*00011*/
+    0, /*00100*/
+    0, /*00101*/
+    0, /*00110*/
+    0, /*00111*/
+    0, /*01000*/
+    0, /*01001*/
+    0, /*01010*/
+    0, /*01011*/
+    0, /*01100*/
+    0, /*01101*/
+    0, /*01110*/
+    0, /*01111*/
+    0, /*10000*/
+    1, /*10001*/
+    1, /*10010*/
+    1, /*10011*/
+    1, /*10100*/
+    1, /*10101*/
+    1, /*10110*/
+    1, /*10111*/
+    0, /*11000*/
+    1, /*11001*/
+    1, /*11010*/
+    1, /*11011*/
+    1, /*11100*/
+    1, /*11101*/
+    1, /*11110*/
+    1, /*11111*/
+};
+// sign, lsb, guard, round, sticky --- rm = 3
+inline constexpr int kRoundUpTable[] = {
+    0, /*00000*/
+    1, /*00001*/
+    1, /*00010*/
+    1, /*00011*/
+    1, /*00100*/
+    1, /*00101*/
+    1, /*00110*/
+    1, /*00111*/
+    0, /*01000*/
+    1, /*01001*/
+    1, /*01010*/
+    1, /*01011*/
+    1, /*01100*/
+    1, /*01101*/
+    1, /*01110*/
+    1, /*01111*/
+    0, /*10000*/
+    0, /*10001*/
+    0, /*10010*/
+    0, /*10011*/
+    0, /*10100*/
+    0, /*10101*/
+    0, /*10110*/
+    0, /*10111*/
+    0, /*11000*/
+    0, /*11001*/
+    0, /*11010*/
+    0, /*11011*/
+    0, /*11100*/
+    0, /*11101*/
+    0, /*11110*/
+    0, /*11111*/
+};
+
+}  // namespace internal
+
+template <typename T>
+class FpConversionsTestHelper {
+  using IntType = typename FPTypeInfo<T>::IntType;
+  using FpType = typename internal::UnsignedToFpType<IntType>::FpType;
+
+ public:
+  // The conversion helper can be used with a float in its floating point format
+  // or with its unsigned integer representation.
+  FpConversionsTestHelper(T value) : fflags_(0) {
+    if constexpr (std::is_same_v<T, IntType>) {
+      unsigned_value_ = value;
+      fp_value_ = absl::bit_cast<FpType>(unsigned_value_);
+    } else if constexpr (std::is_same_v<T, FpType>) {
+      fp_value_ = value;
+      unsigned_value_ = absl::bit_cast<IntType>(fp_value_);
+    }
+  }
+
+  template <typename U>
+  U Convert(FPRoundingMode rm = FPRoundingMode::kRoundToNearest);
+
+  template <typename U>
+  U ConvertWithFlags(uint32_t &fflags,
+                     FPRoundingMode rm = FPRoundingMode::kRoundToNearest) {
+    fflags_ = 0;
+    U ret = Convert<U>(rm);
+    fflags = fflags_;
+    return ret;
+  }
+
+ protected:
+  FpType fp_value_;
+  IntType unsigned_value_;
+  uint32_t fflags_;
+
+  bool sign() {
+    return (unsigned_value_ & (1ULL << (FPTypeInfo<FpType>::kBitSize - 1))) !=
+           0;
+  }
+
+  template <typename IntReturnType, typename FpReturnType>
+  void NarrowingConversionMakeExponentAndSignificand(FPRoundingMode,
+                                                     IntReturnType &,
+                                                     IntReturnType &);
+
+  template <typename IntReturnType, typename FpReturnType>
+  IntReturnType NarrowingConversionHandleInfinity(FPRoundingMode);
+
+  template <typename IntReturnType, typename FpReturnType>
+  IntReturnType NarrowingConversion(FPRoundingMode);
+
+  template <typename U>
+  U RoundingRightShift(U value, int32_t shift_amt, FPRoundingMode rm) {
+    bool guard = 0;
+    bool round = 0;
+    bool sticky = 0;
+    for (int i = 0; i < shift_amt; ++i) {
+      sticky |= round;
+      round = guard;
+      guard = value & 1;
+      value >>= 1;
+    }
+
+    bool lsb = value & 1;
+    uint8_t key = sign() << 4 | lsb << 3 | guard << 2 | round << 1 | sticky;
+    value += GetRoundingTable(rm)[key];
+    return value;
+  }
+
+  const int *GetRoundingTable(FPRoundingMode rm) {
+    switch (rm) {
+      case FPRoundingMode::kRoundToNearest:
+        return static_cast<const int *>(internal::kRoundToNearestTable);
+      case FPRoundingMode::kRoundTowardsZero:
+        return static_cast<const int *>(internal::kRoundTowardsZeroTable);
+      case FPRoundingMode::kRoundDown:
+        return static_cast<const int *>(internal::kRoundDownTable);
+      case FPRoundingMode::kRoundUp:
+        return static_cast<const int *>(internal::kRoundUpTable);
+      default:
+        return static_cast<const int *>(internal::kRoundToNearestTable);
+    }
+  }
+};  // class FpConversionsTestHelper
+
+template <typename T>
+template <typename U>
+U FpConversionsTestHelper<T>::Convert(FPRoundingMode rm) {
+  using IntReturnType = typename FPTypeInfo<U>::IntType;
+  using FpReturnType =
+      typename internal::UnsignedToFpType<IntReturnType>::FpType;
+
+  if constexpr (std::is_same_v<U, IntType>) {
+    return unsigned_value_;
+  } else if constexpr (std::is_same_v<U, FpType>) {
+    return fp_value_;
+  }
+
+  if (FPTypeInfo<FpType>::IsNaN(fp_value_) &&
+      !FPTypeInfo<FpType>::IsQNaN(fp_value_)) {
+    fflags_ |= static_cast<uint32_t>(FPExceptions::kInvalidOp);
+  }
+
+  if (FPTypeInfo<FpType>::IsNaN(fp_value_)) {
+    return absl::bit_cast<U>(FPTypeInfo<FpReturnType>::kCanonicalNaN);
+  } else if (FPTypeInfo<FpType>::kPosInf == unsigned_value_) {
+    return absl::bit_cast<U>(FPTypeInfo<FpReturnType>::kPosInf);
+  } else if (FPTypeInfo<FpType>::kNegInf == unsigned_value_) {
+    return absl::bit_cast<U>(FPTypeInfo<FpReturnType>::kNegInf);
+  } else if (FPTypeInfo<FpType>::kPosZero == unsigned_value_) {
+    return absl::bit_cast<U>(FPTypeInfo<FpReturnType>::kPosZero);
+  } else if (FPTypeInfo<FpType>::kNegZero == unsigned_value_) {
+    return absl::bit_cast<U>(FPTypeInfo<FpReturnType>::kNegZero);
+  }
+
+  if constexpr (std::numeric_limits<IntReturnType>::digits >
+                std::numeric_limits<IntType>::digits) {
+    // The return type is larger so the conversion is simple.
+    FpReturnType mantissa = static_cast<FpReturnType>(
+        unsigned_value_ & FPTypeInfo<FpType>::kSigMask);
+    FpReturnType precision_factor =
+        std::pow(2.0, -static_cast<FpReturnType>(FPTypeInfo<FpType>::kSigSize));
+    IntType biased_exponent =
+        (unsigned_value_ & FPTypeInfo<FpType>::kExpMask) >>
+        FPTypeInfo<FpType>::kSigSize;
+    int32_t unbiased_exponent =
+        (biased_exponent ? static_cast<int32_t>(biased_exponent) : 1) -
+        FPTypeInfo<FpType>::kExpBias;
+
+    // Use the formula from the IEEE754 section 3.4 that details moving from
+    // the binary format to the number being represented.
+    FpReturnType implicit_bit_adjustment = biased_exponent ? 1.0 : 0.0;
+    FpReturnType exponent_factor = std::pow(2.0, unbiased_exponent);
+    FpReturnType unsigned_result =
+        ((mantissa * precision_factor) + implicit_bit_adjustment) *
+        exponent_factor;
+    IntReturnType result = absl::bit_cast<IntReturnType>(unsigned_result) |
+                           (static_cast<IntReturnType>(sign())
+                            << (FPTypeInfo<FpReturnType>::kBitSize - 1));
+    return absl::bit_cast<U>(result);
+  }
+
+  // If the return type is smaller then call through to the narrowing
+  // conversion.
+  return absl::bit_cast<U>(
+      NarrowingConversion<IntReturnType, FpReturnType>(rm));
+}
+
+template <typename T>
+template <typename IntReturnType, typename FpReturnType>
+void FpConversionsTestHelper<T>::NarrowingConversionMakeExponentAndSignificand(
+    FPRoundingMode rm, IntReturnType &out_exponent,
+    IntReturnType &out_significand) {
+  int32_t e_max = FPTypeInfo<FpReturnType>::kExpBias;
+  int32_t e_min = 1 - e_max;
+  IntType in_exponent = (unsigned_value_ & FPTypeInfo<FpType>::kExpMask) >>
+                        FPTypeInfo<FpType>::kSigSize;
+  IntType in_significand = unsigned_value_ & FPTypeInfo<FpType>::kSigMask;
+  // Add the implicit bit to the significand.
+  if (in_exponent) {
+    in_significand |= 1ULL << FPTypeInfo<FpType>::kSigSize;
+  }
+  int32_t exponent_bias_diff =
+      FPTypeInfo<FpType>::kExpBias - FPTypeInfo<FpReturnType>::kExpBias;
+  int32_t unbiased_exponent =
+      static_cast<int32_t>(in_exponent) - FPTypeInfo<FpType>::kExpBias;
+  int32_t significand_size_diff =
+      FPTypeInfo<FpType>::kSigSize - FPTypeInfo<FpReturnType>::kSigSize;
+
+  if (unbiased_exponent < e_min) {
+    // The destination float will be subnormal.
+    out_exponent = 0;
+    int shift_amt = significand_size_diff + (e_min - unbiased_exponent);
+    out_significand = RoundingRightShift(in_significand, shift_amt, rm);
+  } else if (unbiased_exponent > e_max) {
+    // The destination float will be infinity.
+    out_exponent = FPTypeInfo<FpReturnType>::kExpMask >>
+                   FPTypeInfo<FpReturnType>::kSigSize;
+    out_significand = 0;
+  } else {
+    // The destination float will be normal.
+    out_exponent = in_exponent - exponent_bias_diff;
+    out_significand =
+        RoundingRightShift(in_significand & FPTypeInfo<FpType>::kSigMask,
+                           significand_size_diff, rm);
+  }
+  // Rounding can cause the significand to overflow. Remask and increment the
+  // exponent to fix.
+  if ((out_significand & FPTypeInfo<FpReturnType>::kSigMask) !=
+      out_significand) {
+    out_exponent =
+        std::min(out_exponent + 1, FPTypeInfo<FpReturnType>::kExpMask >>
+                                       FPTypeInfo<FpReturnType>::kSigSize);
+    out_significand &= FPTypeInfo<FpReturnType>::kSigMask;
+  }
+}
+
+template <typename T>
+template <typename IntReturnType, typename FpReturnType>
+IntReturnType FpConversionsTestHelper<T>::NarrowingConversionHandleInfinity(
+    FPRoundingMode rm) {
+  IntReturnType out_uint = sign() ? FPTypeInfo<FpReturnType>::kNegInf
+                                  : FPTypeInfo<FpReturnType>::kPosInf;
+  int32_t e_max = FPTypeInfo<FpReturnType>::kExpBias;
+  double fp_value_double = internal::ToDouble(fp_value_);
+  double largest_non_inf_double = internal::ToDouble<IntReturnType>(
+      (sign() ? FPTypeInfo<FpReturnType>::kNegInf - 1
+              : FPTypeInfo<FpReturnType>::kPosInf - 1));
+  // To handle the cases near infinity, we need to consider what the
+  // conversion would have been if the exponent was unbounded.
+  double first_out_of_range_double =
+      std::pow(2.0, e_max + 1) * std::pow(-1.0, sign());
+
+  // Figure out if the input is closer to the largest non-inf or the unbounded
+  // number.
+  double distance_to_largest_non_inf =
+      std::abs(fp_value_double - largest_non_inf_double);
+  double distance_to_first_out_of_range =
+      std::abs(fp_value_double - first_out_of_range_double);
+  switch (rm) {
+    case FPRoundingMode::kRoundToNearest:
+      if (distance_to_largest_non_inf < distance_to_first_out_of_range) {
+        out_uint = sign() ? FPTypeInfo<FpReturnType>::kNegInf - 1
+                          : FPTypeInfo<FpReturnType>::kPosInf - 1;
+      }
+      break;
+    case FPRoundingMode::kRoundTowardsZero:
+      out_uint = sign() ? FPTypeInfo<FpReturnType>::kNegInf - 1
+                        : FPTypeInfo<FpReturnType>::kPosInf - 1;
+      break;
+    case FPRoundingMode::kRoundDown:
+      out_uint = sign() ? FPTypeInfo<FpReturnType>::kNegInf
+                        : FPTypeInfo<FpReturnType>::kPosInf - 1;
+      break;
+    case FPRoundingMode::kRoundUp:
+      out_uint = sign() ? FPTypeInfo<FpReturnType>::kNegInf - 1
+                        : FPTypeInfo<FpReturnType>::kPosInf;
+      break;
+    default:
+      break;
+  }
+  fflags_ |= static_cast<uint32_t>(FPExceptions::kOverflow);
+  fflags_ |= static_cast<uint32_t>(FPExceptions::kInexact);
+  return out_uint;
+}
+
+template <typename T>
+template <typename IntReturnType, typename FpReturnType>
+IntReturnType FpConversionsTestHelper<T>::NarrowingConversion(
+    FPRoundingMode rm) {
+  int32_t e_min = 1 - FPTypeInfo<FpReturnType>::kExpBias;
+  IntReturnType out_exponent = 0;
+  IntReturnType out_significand = 0;
+  NarrowingConversionMakeExponentAndSignificand<IntReturnType, FpReturnType>(
+      rm, out_exponent, out_significand);
+
+  IntReturnType out_uint = sign() ? FPTypeInfo<FpReturnType>::kNegZero
+                                  : FPTypeInfo<FpReturnType>::kPosZero;
+  out_uint |= out_significand & FPTypeInfo<FpReturnType>::kSigMask;
+  out_uint |= (out_exponent << FPTypeInfo<FpReturnType>::kSigSize) &
+              FPTypeInfo<FpReturnType>::kExpMask;
+
+  if (out_uint == FPTypeInfo<FpReturnType>::kPosInf ||
+      out_uint == FPTypeInfo<FpReturnType>::kNegInf) {
+    // Handle rounding and flags for infinity.
+    out_uint =
+        NarrowingConversionHandleInfinity<IntReturnType, FpReturnType>(rm);
+  } else {
+    // Handle the flags not related to infinity.
+    double fp_value_double = internal::ToDouble<FpType>(fp_value_);
+    double result = internal::ToDouble<IntReturnType>(out_uint);
+
+    if (result != fp_value_double) {
+      double b_emin = std::pow(2.0, e_min);
+      if (std::abs(result) <= b_emin || std::abs(fp_value_double) <= b_emin) {
+        fflags_ |= static_cast<uint32_t>(FPExceptions::kUnderflow);
+      }
+      fflags_ |= static_cast<uint32_t>(FPExceptions::kInexact);
+    }
+  }
+  return out_uint;
+}
+
+template <typename T>
+FpConversionsTestHelper(T) -> FpConversionsTestHelper<T>;
+
 }  // namespace test
 }  // namespace riscv
 }  // namespace sim
diff --git a/riscv/test/riscv_zfh_instructions_test.cc b/riscv/test/riscv_zfh_instructions_test.cc
new file mode 100644
index 0000000..1aaa685
--- /dev/null
+++ b/riscv/test/riscv_zfh_instructions_test.cc
@@ -0,0 +1,556 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "riscv/riscv_zfh_instructions.h"
+
+#include <sys/types.h>
+
+#include <algorithm>
+#include <any>
+#include <cassert>
+#include <cmath>
+#include <cstdint>
+#include <ios>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include "absl/base/casts.h"
+#include "googlemock/include/gmock/gmock.h"
+#include "mpact/sim/generic/data_buffer.h"
+#include "mpact/sim/generic/immediate_operand.h"
+#include "mpact/sim/generic/instruction.h"
+#include "mpact/sim/generic/operand_interface.h"
+#include "mpact/sim/generic/type_helpers.h"
+#include "riscv/riscv_fp_info.h"
+#include "riscv/riscv_i_instructions.h"
+#include "riscv/riscv_register.h"
+#include "riscv/test/riscv_fp_test_base.h"
+
+namespace {
+
+using ::mpact::sim::generic::operator*;  // NOLINT: is used below.
+using ::mpact::sim::generic::DataBuffer;
+using ::mpact::sim::generic::HalfFP;
+using ::mpact::sim::generic::ImmediateOperand;
+using ::mpact::sim::generic::Instruction;
+using ::mpact::sim::riscv::FPRoundingMode;
+using ::mpact::sim::riscv::RiscVZfhCvtDh;
+using ::mpact::sim::riscv::RiscVZfhCvtHd;
+using ::mpact::sim::riscv::RiscVZfhCvtHs;
+using ::mpact::sim::riscv::RiscVZfhCvtSh;
+using ::mpact::sim::riscv::RiscVZfhFlhChild;
+using ::mpact::sim::riscv::RiscVZfhFMvhx;
+using ::mpact::sim::riscv::RV32Register;
+using ::mpact::sim::riscv::RVFpRegister;
+using ::mpact::sim::riscv::RV32::RiscVILhu;
+using ::mpact::sim::riscv::RV32::RiscVZfhFMvxh;
+using ::mpact::sim::riscv::test::FpConversionsTestHelper;
+using ::mpact::sim::riscv::test::FPTypeInfo;
+using ::mpact::sim::riscv::test::RiscVFPInstructionTestBase;
+
+const int kRoundingModeRoundToNearest =
+    static_cast<int>(FPRoundingMode::kRoundToNearest);
+const int kRoundingModeRoundTowardsZero =
+    static_cast<int>(FPRoundingMode::kRoundTowardsZero);
+const int kRoundingModeRoundDown = static_cast<int>(FPRoundingMode::kRoundDown);
+const int kRoundingModeRoundUp = static_cast<int>(FPRoundingMode::kRoundUp);
+
+// A source operand that is used to set the rounding mode. This is less
+// confusing than using a register source operand since the rounding mode is
+// part of the instruction encoding.
+class TestRoundingModeSourceOperand
+    : public mpact::sim::generic::SourceOperandInterface {
+ public:
+  explicit TestRoundingModeSourceOperand()
+      : rounding_mode_(FPRoundingMode::kRoundToNearest) {}
+
+  void SetRoundingMode(FPRoundingMode rounding_mode) {
+    rounding_mode_ = rounding_mode;
+  }
+
+  bool AsBool(int) override { return static_cast<bool>(rounding_mode_); }
+  int8_t AsInt8(int) override { return static_cast<int8_t>(rounding_mode_); }
+  uint8_t AsUint8(int) override { return static_cast<uint8_t>(rounding_mode_); }
+  int16_t AsInt16(int) override { return static_cast<int16_t>(rounding_mode_); }
+  uint16_t AsUint16(int) override {
+    return static_cast<uint16_t>(rounding_mode_);
+  }
+  int32_t AsInt32(int) override { return static_cast<int32_t>(rounding_mode_); }
+  uint32_t AsUint32(int) override {
+    return static_cast<uint32_t>(rounding_mode_);
+  }
+  int64_t AsInt64(int) override { return static_cast<int64_t>(rounding_mode_); }
+  uint64_t AsUint64(int) override {
+    return static_cast<uint64_t>(rounding_mode_);
+  }
+
+  std::vector<int> shape() const override { return {1}; }
+  std::string AsString() const override { return std::string(""); }
+  std::any GetObject() const override { return std::any(); }
+
+ protected:
+  FPRoundingMode rounding_mode_;
+};
+
+class RV32ZfhInstructionTest : public RiscVFPInstructionTestBase {
+ protected:
+  // Test conversion instructions. The instance variable semantic_function_ is
+  // used to set the semantic function for the instruction and should be set
+  // before calling this function.
+  template <typename T, typename U, int rm = 0>
+  T ConversionHelper(U input_val) {
+    // Initialize a fresh instruction.
+    ResetInstruction();
+    assert(semantic_function_);
+    SetSemanticFunction(semantic_function_);
+
+    // Configure source and destination operands for the instruction.
+    AppendRegisterOperands<RVFpRegister>({"f1"}, {"f5"});
+    instruction_->AppendSource(new TestRoundingModeSourceOperand());
+    auto *flag_op = rv_fp_->fflags()->CreateSetDestinationOperand(0, "fflags");
+    instruction_->AppendDestination(flag_op);
+    assert(instruction_->SourcesSize() == 2);
+    assert(instruction_->DestinationsSize() == 2);
+
+    // Set all operands to known values before executing the instruction.
+    static_cast<TestRoundingModeSourceOperand *>(instruction_->Source(1))
+        ->SetRoundingMode(static_cast<FPRoundingMode>(rm));
+    rv_fp_->SetRoundingMode(static_cast<FPRoundingMode>(rm));
+    SetNaNBoxedRegisterValues<U, RVFpRegister>({{"f1", input_val}});
+    SetRegisterValues<int, RVFpRegister>({{"f5", 0xDEAFBEEFDEADBEEF}});
+    rv_fp_->fflags()->Write(static_cast<uint32_t>(0));
+
+    instruction_->Execute(nullptr);
+    T reg_val = state_->GetRegister<RVFpRegister>("f5")
+                    .first->data_buffer()
+                    ->template Get<T>(0);
+    return reg_val;
+  }
+
+  template <FPRoundingMode>
+  void RoundingConversionTestHelper(uint32_t, uint16_t, uint32_t &, uint32_t,
+                                    uint16_t, uint32_t &);
+
+  template <FPRoundingMode rm>
+  void RoundingPointTest(uint16_t);
+
+  template <typename T>
+  void SetupMemory(uint64_t, T);
+
+  template <typename T, typename IntegerRegister>
+  T LoadHalfHelper(uint64_t, int16_t);
+
+  Instruction::SemanticFunction semantic_function_ = nullptr;
+};
+
+template <FPRoundingMode rm>
+void RV32ZfhInstructionTest::RoundingConversionTestHelper(
+    uint32_t float_uint_before, uint16_t half_uint_before,
+    uint32_t &first_expected_fflags, uint32_t float_uint_after,
+    uint16_t half_uint_after, uint32_t &second_expected_fflags) {
+  float input_val;
+  HalfFP expected_val;
+  HalfFP actual_val;
+
+  input_val = absl::bit_cast<float>(float_uint_before);
+  expected_val = {.value = half_uint_before};
+  actual_val = ConversionHelper<HalfFP, float, static_cast<int>(rm)>(input_val);
+  EXPECT_EQ(expected_val.value, actual_val.value)
+      << "expected: " << std::hex << expected_val.value
+      << ", actual: " << std::hex << actual_val.value
+      << ", float_uint: " << std::hex << float_uint_before
+      << ", rounding_mode: " << static_cast<int>(rm);
+  EXPECT_EQ(first_expected_fflags, rv_fp_->fflags()->GetUint32())
+      << "while converting: " << std::hex << float_uint_before
+      << " to:" << std::hex << actual_val.value
+      << " with rounding mode: " << static_cast<int>(rm);
+
+  input_val = absl::bit_cast<float>(float_uint_after);
+  expected_val = {.value = half_uint_after};
+  actual_val = ConversionHelper<HalfFP, float, static_cast<int>(rm)>(input_val);
+  EXPECT_EQ(expected_val.value, actual_val.value)
+      << "expected: " << std::hex << expected_val.value
+      << ", actual: " << std::hex << actual_val.value
+      << ", float_uint: " << std::hex << float_uint_after
+      << ", rounding_mode: " << static_cast<int>(rm);
+  EXPECT_EQ(second_expected_fflags, rv_fp_->fflags()->GetUint32())
+      << "while converting: " << std::hex << float_uint_after
+      << " to:" << std::hex << actual_val.value
+      << " with rounding mode: " << static_cast<int>(rm);
+}
+
+template <typename T>
+void RV32ZfhInstructionTest::SetupMemory(uint64_t address, T value) {
+  DataBuffer *mem_db = state_->db_factory()->Allocate<T>(1);
+  mem_db->Set<T>(0, value);
+  state_->StoreMemory(instruction_, address, mem_db);
+  mem_db->DecRef();
+}
+
+template <typename T, typename IntegerRegister>
+T RV32ZfhInstructionTest::LoadHalfHelper(uint64_t base, int16_t offset) {
+  const std::string kRs1Name("x1");
+  const std::string kFrdName("f5");
+  AppendRegisterOperands<IntegerRegister>({kRs1Name}, {});
+  AppendRegisterOperands<RVFpRegister>(child_instruction_, {}, {kFrdName});
+
+  ImmediateOperand<int16_t> *offset_source_operand =
+      new ImmediateOperand<int16_t>(offset);
+  instruction_->AppendSource(offset_source_operand);
+
+  SetRegisterValues<typename IntegerRegister::ValueType, IntegerRegister>(
+      {{kRs1Name, static_cast<IntegerRegister::ValueType>(base)}});
+  SetRegisterValues<uint32_t, RVFpRegister>({{kFrdName, 0}});
+
+  instruction_->Execute(nullptr);
+
+  T observed_val = state_->GetRegister<RVFpRegister>(kFrdName)
+                       .first->data_buffer()
+                       ->template Get<T>(0);
+  return observed_val;
+}
+
+// Test the FP16 load instruction. The semantic functions should match the isa
+// file.
+TEST_F(RV32ZfhInstructionTest, RiscVFlh) {
+  SetSemanticFunction(&RiscVILhu);
+  SetChildInstruction();
+  SetChildSemanticFunction(&RiscVZfhFlhChild);
+
+  SetupMemory<uint16_t>(0xFF, 0xBEEF);
+
+  HalfFP observed_val =
+      LoadHalfHelper<HalfFP, RV32Register>(/* base */ 0x0, /* offset */ 0xFF);
+  EXPECT_EQ(observed_val.value, 0xBEEF);
+}
+
+// Test the FP16 load instruction. When looking at the register contents as a
+// float, it should be NaN.
+TEST_F(RV32ZfhInstructionTest, RiscVFlh_float_nanbox) {
+  SetSemanticFunction(&RiscVILhu);
+  SetChildInstruction();
+  SetChildSemanticFunction(&RiscVZfhFlhChild);
+
+  SetupMemory<uint16_t>(0xFF, 0xBEEF);
+
+  float observed_val =
+      LoadHalfHelper<float, RV32Register>(/* base */ 0xFF, /* offset */ 0);
+  EXPECT_TRUE(std::isnan(observed_val));
+}
+
+// Test the FP16 load instruction. When looking at the register contents as a
+// double, it should be NaN.
+TEST_F(RV32ZfhInstructionTest, RiscVFlh_double_nanbox) {
+  SetSemanticFunction(&RiscVILhu);
+  SetChildInstruction();
+  SetChildSemanticFunction(&RiscVZfhFlhChild);
+
+  SetupMemory<uint16_t>(0xFF, 0xBEEF);
+
+  double observed_val = LoadHalfHelper<double, RV32Register>(
+      /* base */ 0x0100, /* offset */ -1);
+  EXPECT_TRUE(std::isnan(observed_val));
+}
+
+// Move half precision from a float register to an integer register. The IEEE754
+// encoding is preserved in the integer register.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhFMvxh) {
+  SetSemanticFunction(&RiscVZfhFMvxh);
+  UnaryOpFPTestHelper<uint32_t, HalfFP>(
+      "fmv.x.h", instruction_, {"f", "x"}, 32, [](HalfFP half_fp) -> uint32_t {
+        bool sign = 1 & (half_fp.value >> (FPTypeInfo<HalfFP>::kBitSize - 1));
+        // Fill the upper XLEN-16 bits with the sign bit as per the spec.
+        uint32_t result = sign ? 0xFFFF'0000 : 0;
+        result |= static_cast<uint32_t>(half_fp.value);
+        return result;
+      });
+}
+
+// Move half precision from an integer register (lower 16 bits) to a float
+// register.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhFMvhx) {
+  SetSemanticFunction(&RiscVZfhFMvhx);
+  UnaryOpFPTestHelper<HalfFP, uint64_t>(
+      "fmv.h.x", instruction_, {"x", "f"}, 32, [](uint64_t scalar) -> HalfFP {
+        return HalfFP{.value = static_cast<uint16_t>(scalar)};
+      });
+}
+
+// Half precision to single precision conversion.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhCvtSh) {
+  SetSemanticFunction(&RiscVZfhCvtSh);
+  UnaryOpWithFflagsFPTestHelper<float, HalfFP>(
+      "fcvt.s.h", instruction_, {"f", "f"}, 32,
+      [](HalfFP half_fp, int rm) -> std::tuple<float, uint32_t> {
+        uint32_t fflags = 0;
+        float float_result =
+            FpConversionsTestHelper(half_fp).ConvertWithFlags<float>(
+                fflags, static_cast<FPRoundingMode>(rm));
+        return std::make_tuple(float_result, fflags);
+      });
+}
+
+// Single precision to half precision conversion.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhCvtHs) {
+  SetSemanticFunction(&RiscVZfhCvtHs);
+  UnaryOpWithFflagsFPTestHelper<HalfFP, float>(
+      "fcvt.h.s", instruction_, {"f", "f"}, 32,
+      [](float input_float, int rm) -> std::tuple<HalfFP, uint32_t> {
+        uint32_t fflags = 0;
+        HalfFP half_result = FpConversionsTestHelper(input_float)
+                                 .ConvertWithFlags<HalfFP>(
+                                     fflags, static_cast<FPRoundingMode>(rm));
+        return std::make_tuple(half_result, fflags);
+      });
+}
+
+// Find the nearest floats that convert to the given half precision values.
+std::tuple<uint32_t, uint32_t> GetRoundingPoints(uint16_t first,
+                                                 uint16_t second,
+                                                 FPRoundingMode rm) {
+  uint16_t upper_uhalf = std::max(first, second);
+  uint32_t upper_ufloat = absl::bit_cast<uint32_t>(
+      FpConversionsTestHelper(upper_uhalf).Convert<float>(rm));
+
+  uint16_t lower_uhalf = std::min(first, second);
+  uint32_t lower_ufloat = absl::bit_cast<uint32_t>(
+      FpConversionsTestHelper(lower_uhalf).Convert<float>(rm));
+  while (upper_ufloat - lower_ufloat > 1) {
+    uint32_t udelta = upper_ufloat - lower_ufloat;
+    uint32_t mid_ufloat = lower_ufloat + (udelta >> 1);
+    HalfFP mid_half = FpConversionsTestHelper(mid_ufloat).Convert<HalfFP>(rm);
+    uint16_t mid_uhalf = mid_half.value;
+    if (upper_uhalf == mid_uhalf) {
+      upper_ufloat = mid_ufloat;
+    } else if (lower_uhalf == mid_uhalf) {
+      lower_ufloat = mid_ufloat;
+    }
+  }
+  if (first > second) {
+    return std::make_tuple(upper_ufloat, lower_ufloat);
+  }
+  return std::make_tuple(lower_ufloat, upper_ufloat);
+}
+
+template <FPRoundingMode rm>
+void RV32ZfhInstructionTest::RoundingPointTest(uint16_t base_uhalf) {
+  uint16_t first_uhalf = base_uhalf, second_uhalf = base_uhalf + 1;
+  uint32_t first_ufloat, second_ufloat;
+  uint32_t first_expected_fflags = 0, second_expected_fflags = 0;
+
+  std::tie(first_ufloat, second_ufloat) =
+      GetRoundingPoints(first_uhalf, second_uhalf, rm);
+  // Get the expected fflags
+  FpConversionsTestHelper(first_ufloat)
+      .ConvertWithFlags<HalfFP>(first_expected_fflags, rm);
+  FpConversionsTestHelper(second_ufloat)
+      .ConvertWithFlags<HalfFP>(second_expected_fflags, rm);
+  RoundingConversionTestHelper<rm>(first_ufloat, first_uhalf,
+                                   first_expected_fflags, second_ufloat,
+                                   second_uhalf, second_expected_fflags);
+}
+
+// Verify that the rounding points in the semantic functions match the
+// rounding points in the test helpers. Test across all rounding modes.
+TEST_F(RV32ZfhInstructionTest,
+       RiscVZfhCvtHs_conversion_rounding_points_first_nonzero) {
+  semantic_function_ = &RiscVZfhCvtHs;
+  // The first float that converts to a non zero half precision value after
+  // rounding. Zero before, denormal after.
+  uint16_t pos_uhalf = 0b0'00000'0000000000;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(pos_uhalf);
+
+  uint16_t neg_uhalf = 0b1'00000'0000000000;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(neg_uhalf);
+}
+
+TEST_F(RV32ZfhInstructionTest,
+       RiscVZfhCvtHs_conversion_rounding_points_denrom_denorm) {
+  semantic_function_ = &RiscVZfhCvtHs;
+
+  // Rounding denormal before and denormal after
+  uint16_t pos_uhalf = 0b0'00000'0000000001;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(pos_uhalf);
+
+  uint16_t neg_uhalf = 0b1'00000'0000000001;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(neg_uhalf);
+}
+
+TEST_F(RV32ZfhInstructionTest,
+       RiscVZfhCvtHs_conversion_rounding_points_denorm_normal) {
+  semantic_function_ = &RiscVZfhCvtHs;
+
+  // The rounding overflows the significand and should increase the exponent.
+  // Denormal before, normal after.
+  uint16_t pos_uhalf = 0b0'00000'1111111111;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(pos_uhalf);
+
+  uint16_t neg_uhalf = 0b1'00000'1111111111;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(neg_uhalf);
+}
+
+TEST_F(RV32ZfhInstructionTest,
+       RiscVZfhCvtHs_conversion_rounding_points_normal_normal) {
+  semantic_function_ = &RiscVZfhCvtHs;
+
+  // Rounding normal before and normal after.
+  uint16_t pos_uhalf = 0b0'11110'1111111110;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(pos_uhalf);
+
+  uint16_t neg_uhalf = 0b1'11110'1111111110;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(neg_uhalf);
+}
+
+TEST_F(RV32ZfhInstructionTest, RiscVZfhCvtHs_conversion_rounding_points_inf) {
+  semantic_function_ = &RiscVZfhCvtHs;
+
+  // Rounding normal before and infinity after.
+  uint16_t pos_uhalf = 0b0'11110'1111111111;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(pos_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(pos_uhalf);
+
+  uint16_t neg_uhalf = 0b1'11110'1111111111;
+  RoundingPointTest<FPRoundingMode::kRoundToNearest>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundTowardsZero>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundDown>(neg_uhalf);
+  RoundingPointTest<FPRoundingMode::kRoundUp>(neg_uhalf);
+}
+
+// Half precision to double precision conversion.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhCvtDh) {
+  SetSemanticFunction(&RiscVZfhCvtDh);
+  UnaryOpWithFflagsFPTestHelper<double, HalfFP>(
+      "fcvt.d.h", instruction_, {"f", "f"}, 32,
+      [](HalfFP half_fp, int rm) -> std::tuple<double, uint32_t> {
+        uint32_t fflags = 0;
+        double double_result =
+            FpConversionsTestHelper(half_fp).ConvertWithFlags<double>(
+                fflags, static_cast<FPRoundingMode>(rm));
+        return std::make_tuple(double_result, fflags);
+      });
+}
+
+// Double precision to half precision conversion.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhCvtHd) {
+  SetSemanticFunction(&RiscVZfhCvtHd);
+  UnaryOpWithFflagsFPTestHelper<HalfFP, double>(
+      "fcvt.h.d", instruction_, {"f", "f"}, 32,
+      [](double input_double, int rm) -> std::tuple<HalfFP, uint32_t> {
+        uint32_t fflags = 0;
+        HalfFP half_result = FpConversionsTestHelper(input_double)
+                                 .ConvertWithFlags<HalfFP>(
+                                     fflags, static_cast<FPRoundingMode>(rm));
+        return std::make_tuple(half_result, fflags);
+      });
+}
+
+// A test to make sure +0 and -0 are converted correctly. Mutation testing
+// inspired this test.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhCvtSh_strict_zeros) {
+  semantic_function_ = &RiscVZfhCvtSh;
+  uint32_t expected_p0 = FPTypeInfo<float>::kPosZero;
+  uint32_t expected_n0 = FPTypeInfo<float>::kNegZero;
+  float actual_p0;
+  float actual_n0;
+  HalfFP pos_zero = {.value = FPTypeInfo<HalfFP>::kPosZero};
+  HalfFP neg_zero = {.value = FPTypeInfo<HalfFP>::kNegZero};
+
+  actual_p0 =
+      ConversionHelper<float, HalfFP, kRoundingModeRoundToNearest>(pos_zero);
+  actual_n0 =
+      ConversionHelper<float, HalfFP, kRoundingModeRoundToNearest>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_n0), expected_n0);
+
+  actual_p0 =
+      ConversionHelper<float, HalfFP, kRoundingModeRoundTowardsZero>(pos_zero);
+  actual_n0 =
+      ConversionHelper<float, HalfFP, kRoundingModeRoundTowardsZero>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_n0), expected_n0);
+
+  actual_p0 = ConversionHelper<float, HalfFP, kRoundingModeRoundDown>(pos_zero);
+  actual_n0 = ConversionHelper<float, HalfFP, kRoundingModeRoundDown>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_n0), expected_n0);
+
+  actual_p0 = ConversionHelper<float, HalfFP, kRoundingModeRoundUp>(pos_zero);
+  actual_n0 = ConversionHelper<float, HalfFP, kRoundingModeRoundUp>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint32_t>(actual_n0), expected_n0);
+}
+
+// A test to make sure +0 and -0 are converted correctly. Mutation testing
+// inspired this test.
+TEST_F(RV32ZfhInstructionTest, RiscVZfhCvtHs_strict_zeros) {
+  semantic_function_ = &RiscVZfhCvtHs;
+  uint16_t expected_p0 = FPTypeInfo<HalfFP>::kPosZero;
+  uint16_t expected_n0 = FPTypeInfo<HalfFP>::kNegZero;
+  HalfFP actual_p0;
+  HalfFP actual_n0;
+  float pos_zero = absl::bit_cast<float>(FPTypeInfo<float>::kPosZero);
+  float neg_zero = absl::bit_cast<float>(FPTypeInfo<float>::kNegZero);
+  actual_p0 =
+      ConversionHelper<HalfFP, float, kRoundingModeRoundToNearest>(pos_zero);
+  actual_n0 =
+      ConversionHelper<HalfFP, float, kRoundingModeRoundToNearest>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_n0), expected_n0);
+
+  actual_p0 =
+      ConversionHelper<HalfFP, float, kRoundingModeRoundTowardsZero>(pos_zero);
+  actual_n0 =
+      ConversionHelper<HalfFP, float, kRoundingModeRoundTowardsZero>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_n0), expected_n0);
+
+  actual_p0 = ConversionHelper<HalfFP, float, kRoundingModeRoundDown>(pos_zero);
+  actual_n0 = ConversionHelper<HalfFP, float, kRoundingModeRoundDown>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_n0), expected_n0);
+
+  actual_p0 = ConversionHelper<HalfFP, float, kRoundingModeRoundUp>(pos_zero);
+  actual_n0 = ConversionHelper<HalfFP, float, kRoundingModeRoundUp>(neg_zero);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_p0), expected_p0);
+  EXPECT_EQ(absl::bit_cast<uint16_t>(actual_n0), expected_n0);
+}
+
+}  // namespace