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 *>(®); + 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 *>(®); + 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