Repository: apytypes/apytypes (language: python)

Issue:
Method for converting fractional bits to integer representation
When APyFixed is used for the mantissa in APyFloat, something along the lines of
https://github.com/apytypes/apytypes/blob/2d8c6a432fd955c6a5708a84ddf84f237b8b53e7/src/apyfloat.cc#L834

is used for converting back to the integer representation. However this has two problems:
1. Not all 64-bit integers can be represented by a double.
2. It is inefficient.

It should instead be replaced by a function in APyFixed that takes that the first x-number of fractional bits and returns it as an integer.


Discussion / hints:

Fix patch:
diff --git a/src/apyfixed.h b/src/apyfixed.h
index f9fb1ca9a..cf00ac192 100644
--- a/src/apyfixed.h
+++ b/src/apyfixed.h
@@ -243,6 +243,10 @@ class APyFixed {
     //! `n`)
     bool positive_greater_than_equal_pow2(int n) const;
 
+    //! Retrieve the least significant 64-bits from the underlying limb-vector
+    //! (convenience function used in `APyFloat`)
+    uint64_t get_lsbs() const { return uint64_t_from_limb_vector(_data, 0); }
+
     /* ****************************************************************************** *
      *                           Conversion to other types                            *
      * ****************************************************************************** */
diff --git a/src/apyfloat.cc b/src/apyfloat.cc
index 9ffc061e9..7bec872ea 100644
--- a/src/apyfloat.cc
+++ b/src/apyfloat.cc
@@ -895,9 +895,7 @@ APyFloat APyFloat::operator+(const APyFloat& rhs) const
         apy_res = apy_res - fx_one;
     }
 
-    apy_res <<= res.man_bits;
-    // TODO: Get bit-pattern directly
-    res.man = (man_t)(apy_res).to_double();
+    res.man = apy_res.get_lsbs();
     res.exp = new_exp;
     return res;
 }
@@ -1035,9 +1033,7 @@ APyFloat& APyFloat::operator+=(const APyFloat& rhs)
         apy_res = apy_res - fx_one;
     }
 
-    apy_res <<= man_bits;
-    // TODO: Get bit-pattern directly
-    man = (man_t)(apy_res).to_double();
+    man = apy_res.get_lsbs();
     return *this;
 }
 
diff --git a/src/apyfloat_util.cc b/src/apyfloat_util.cc
index 7f5bec5dd..bf0dd803c 100644
--- a/src/apyfloat_util.cc
+++ b/src/apyfloat_util.cc
@@ -48,6 +48,7 @@ void quantize_apymantissa(
         );
         APyFixed rnd_num(64 * 3, 64 - bits, rnd_data);
         apyman = apyman + rnd_num;
+        apyman = apyman.cast_no_overflow(2 + bits, 2, QuantizationMode::TRN);
     } else if (quantization == QuantizationMode::STOCH_EQUAL) {
         const mp_limb_t rnd = -(random_number_float() % 2);
         std::vector<mp_limb_t> rnd_data = limb_vector_from_uint64_t({ rnd, rnd, 0 });
diff --git a/src/apytypes_util.h b/src/apytypes_util.h
index 5685db134..0e660beaf 100644
--- a/src/apytypes_util.h
+++ b/src/apytypes_util.h
@@ -988,12 +988,13 @@ limb_vector_from_uint64_t(std::initializer_list<uint64_t> list)
     return result;
 }
 
-//! Read a 64-bit value from a limb vector. If `_LIMB_SIZE_BITS == 64`, this results in
-//! a normal vector read without bounds checking. If `_LIMB_SIZE_BITS == 32`, the second
-//! 32-bits limb is bound-checked and the result upper 32-bits are zeroed if
+//! Read an unsigned 64-bit value from a limb vector. If `_LIMB_SIZE_BITS == 64`, this
+//! results in a normal vector read without bounds checking. If `_LIMB_SIZE_BITS == 32`,
+//! the second 32-bits limb is bound-checked and the result upper 32-bits are zeroed if
 //! out-of-bounds.
+template <typename VECTOR_TYPE>
 [[maybe_unused, nodiscard]] static APY_INLINE uint64_t
-uint64_t_from_limb_vector(const std::vector<mp_limb_t>& limb_vec, std::size_t n)
+uint64_t_from_limb_vector(const VECTOR_TYPE& limb_vec, std::size_t n)
 {
     static_assert(_LIMB_SIZE_BITS == 32 || _LIMB_SIZE_BITS == 64);
     if constexpr (_LIMB_SIZE_BITS == 64) {


Test patch:
diff --git a/lib/test/apyfloat/test_arithmetic.py b/lib/test/apyfloat/test_arithmetic.py
index 2b84ceb50..ec8fb3f30 100644
--- a/lib/test/apyfloat/test_arithmetic.py
+++ b/lib/test/apyfloat/test_arithmetic.py
@@ -1146,3 +1146,16 @@ def test_operation_with_numbers():
     assert (0.0 - a).is_identical(-a)
     assert (a * 1.0).is_identical(a)
     assert (1.0 * a).is_identical(a)
+
+
+def test_mantissa_full_range():
+    """
+    Support for utilizing the full mantissa range during addition/subraction was added
+    in #440
+    """
+    a = APyFloat(sign=0, exp=1023, man=0, exp_bits=11, man_bits=61)
+    b = APyFloat(sign=0, exp=1023 - 61, man=0, exp_bits=11, man_bits=61)
+    assert (a + b) == APyFloat(sign=0, exp=1023, man=1, exp_bits=11, man_bits=61)
+    assert (a - b) == APyFloat(
+        sign=0, exp=1022, man=(1 << 61) - 2, exp_bits=11, man_bits=61
+    )
diff --git a/lib/test/apyfloatarray/test_matmul.py b/lib/test/apyfloatarray/test_matmul.py
index 47b2a4046..2643f3143 100644
--- a/lib/test/apyfloatarray/test_matmul.py
+++ b/lib/test/apyfloatarray/test_matmul.py
@@ -270,7 +270,7 @@ def test_inner_product_with_cancellation_long():
     a = APyFloatArray([0, 0, 0], [511, 511, 500], [0, 3, 2], exp_bits=10, man_bits=60)
     b = APyFloatArray([0, 1, 0], [511, 511, 500], [3, 1, 2], exp_bits=10, man_bits=60)
     assert (a @ b).is_identical(b @ a)
-    assert (b @ a).is_identical(APyFloat(0, 488, 1152921504598458368, 10, 60))
+    assert (b @ a).is_identical(APyFloat(0, 488, 1152921504598458376, 10, 60))
     assert (a @ b).is_identical(sum(a * b))