Repository: alugowski/fast_matrix_market (language: python)

Issue:
Test C++23 Fixed width floating-point types
See https://en.cppreference.com/w/cpp/types/floating-point

C++23 offers fixed-width floating point types `std::float16_t` through `std::float128_t` and `std::bfloat16_t`.

These likely already work on toolchains that offer both the types and their respective `<charconv>`.

Possible requirements for testing:
 * detect toolchain support for types and charconv support
 * if support detected, add optional `test_cpp23` that tests these types
 * add a runner with a supporting compiler. [Probably GCC 13](https://en.cppreference.com/w/cpp/compiler_support/23)


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index c7d6e82..d4bdc3a 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,7 @@ Then simply run all the cells in the [benchmark_plots/plot.ipynb](benchmark_plot
   * Support all C++ types.
     * `float`, `double`, `long double`, `std::complex<>`, integer types, `bool`.
     * Arbitrary types. `std::string` comes bundled. See [implementation](include/fast_matrix_market/app/user_type_string.hpp), [example usage](tests/user_type_test.cpp)
+    * C++23 fixed width floating point types like `std::float32_t`.
 
   * Automatic `std::complex` up-cast. For example, `real` files can be read into `std::complex<double>` arrays.
 
diff --git a/include/fast_matrix_market/field_conv.hpp b/include/fast_matrix_market/field_conv.hpp
index 503da5e..a46e545 100644
--- a/include/fast_matrix_market/field_conv.hpp
+++ b/include/fast_matrix_market/field_conv.hpp
@@ -6,6 +6,7 @@
 
 #include <charconv>
 #include <cmath>
+#include <cstring>
 #include <complex>
 #include <limits>
 #include <iomanip>
@@ -179,7 +180,7 @@ namespace fast_matrix_market {
                     throw out_of_range("Floating-point overflow");
                 } else {
                     // std::from_chars does not return a best match on under/overflow, so fall back to strtod
-                    out = std::strtod(pos, nullptr);
+                    out = static_cast<FT>(std::strtod(pos, nullptr));
                 }
             } else {
                 throw invalid_mm("Invalid floating-point value.");
@@ -231,13 +232,22 @@ namespace fast_matrix_market {
 
     template <typename FT>
     const char* read_float(const char* pos, const char* end, FT& out, out_of_range_behavior oorb) {
+        constexpr bool have_fast_float =
 #ifdef FMM_USE_FAST_FLOAT
-        return read_float_fast_float(pos, end, out, oorb);
-#elif defined(FMM_FROM_CHARS_DOUBLE_SUPPORTED)
-        return read_float_from_chars(pos, end, out, oorb);
+        true;
 #else
-        return read_float_fallback(pos, end, out, oorb);
+        false;
+#endif
+
+        if constexpr (have_fast_float && (std::is_same_v<FT, float> || std::is_same_v<FT, double>)) {
+            return read_float_fast_float(pos, end, out, oorb);
+        } else {
+#if defined(FMM_FROM_CHARS_DOUBLE_SUPPORTED)
+            return read_float_from_chars(pos, end, out, oorb);
+#else
+            return read_float_fallback(pos, end, out, oorb);
 #endif
+        }
     }
 
 #ifdef FMM_FROM_CHARS_LONG_DOUBLE_SUPPORTED
@@ -315,15 +325,8 @@ namespace fast_matrix_market {
         return ret;
     }
 
-    inline const char* read_value(const char* pos, const char* end, float& out, const read_options& options = {}) {
-        return read_float(pos, end, out, options.float_out_of_range_behavior);
-    }
-
-    inline const char* read_value(const char* pos, const char* end, double& out, const read_options& options = {}) {
-        return read_float(pos, end, out, options.float_out_of_range_behavior);
-    }
-
-    inline const char* read_value(const char* pos, const char* end, long double& out, const read_options& options = {}) {
+    template <typename T, typename std::enable_if<std::is_floating_point_v<T>, int>::type = 0>
+    const char* read_value(const char* pos, const char* end, T& out, const read_options& options = {}) {
         return read_float(pos, end, out, options.float_out_of_range_behavior);
     }
 
@@ -389,11 +392,8 @@ namespace fast_matrix_market {
         return value ? "1" : "0";
     }
 
-    inline std::string value_to_string(const int32_t & value, [[maybe_unused]] int precision) {
-        return int_to_string(value);
-    }
-
-    inline std::string value_to_string(const int64_t & value, [[maybe_unused]] int precision) {
+    template <typename T, typename std::enable_if<std::is_integral_v<T>, int>::type = 0>
+    std::string value_to_string(const T& value, [[maybe_unused]] int precision) {
         return int_to_string(value);
     }
 
@@ -404,7 +404,17 @@ namespace fast_matrix_market {
     std::string value_to_string_fallback(const T& value, int precision) {
         if (precision < 0) {
             // shortest representation
-            return std::to_string(value);
+            if constexpr (std::is_floating_point_v<T>
+                            && !std::is_same_v<T, float>
+                            && !std::is_same_v<T, double>
+                            && !std::is_same_v<T, long double>) {
+                // std::to_string is faster but does not support fixed-width floating point types.
+                std::ostringstream oss;
+                oss << value;
+                return oss.str();
+            } else {
+                return std::to_string(value);
+            }
         } else {
             std::ostringstream oss;
             oss << std::setprecision(precision) << value;
@@ -523,8 +533,9 @@ namespace fast_matrix_market {
 #endif
 
 #ifdef FMM_TO_CHARS_DOUBLE_SUPPORTED
-    inline std::string value_to_string_to_chars(const float& value, int precision) {
-        std::string ret(16, ' ');
+    template <typename T, typename std::enable_if<std::is_floating_point<T>::value, int>::type = 0>
+    inline std::string value_to_string_to_chars(const T& value, int precision) {
+        std::string ret(100, ' ');
         std::to_chars_result result{};
         if (precision < 0) {
             // shortest representation
@@ -541,23 +552,6 @@ namespace fast_matrix_market {
         }
     }
 
-    inline std::string value_to_string_to_chars(const double& value, int precision) {
-        std::string ret(25, ' ');
-        std::to_chars_result result{};
-        if (precision < 0) {
-            // shortest representation
-            result = std::to_chars(ret.data(), ret.data() + ret.size(), value);
-        } else {
-            // explicit precision
-            result = std::to_chars(ret.data(), ret.data() + ret.size(), value, std::chars_format::general, precision);
-        }
-        if (result.ec == std::errc()) {
-            ret.resize(result.ptr - ret.data());
-            return ret;
-        } else {
-            return value_to_string_fallback(value, precision);
-        }
-    }
 #endif
 
 #ifdef FMM_TO_CHARS_LONG_DOUBLE_SUPPORTED
@@ -581,80 +575,66 @@ namespace fast_matrix_market {
 #endif
 
     /**
-     * float to string.
+     * long double to string.
      *
-     * Preference order: Dragonbox (no precision support), to_chars, Ryu, fallback.
+     * Preference order: to_chars, fallback.
+     * Note: Ryu's generic_128 can do this on some platforms, but it is not reliable.
+     * see https://github.com/ulfjack/ryu/issues/215
      */
-    inline std::string value_to_string(const float& value, int precision) {
-#ifdef FMM_USE_DRAGONBOX
-        if (precision < 0) {
-            // Shortest representation. Dragonbox is fastest.
-            return value_to_string_dragonbox(value);
-        }
-#endif
-
-#if defined(FMM_TO_CHARS_DOUBLE_SUPPORTED)
+    inline std::string value_to_string(const long double& value, int precision) {
+#if defined(FMM_TO_CHARS_LONG_DOUBLE_SUPPORTED)
         return value_to_string_to_chars(value, precision);
-#elif defined(FMM_USE_RYU)
-        return value_to_string_ryu(value, precision);
 #else
         return value_to_string_fallback(value, precision);
 #endif
     }
 
+
     /**
-     * double to string.
+     * floating-point to string.
+     *
+     * Preference order: Dragonbox (fast but no precision support), to_chars, Ryu, fallback.
      *
-     * Preference order: Dragonbox (no precision support), to_chars, Ryu, fallback.
+     * Dragonbox and Ryu only support float and double types.
      */
-    inline std::string value_to_string(const double& value, int precision) {
+    template <typename T, typename std::enable_if<std::is_floating_point_v<T> && !std::is_same_v<T, long double>, int>::type = 0>
+    std::string value_to_string(const T& value, int precision) {
 #ifdef FMM_USE_DRAGONBOX
-        if (precision < 0) {
-            // Shortest representation. Dragonbox is fastest.
-            return value_to_string_dragonbox(value);
+        if constexpr (std::is_same_v<T, float> || std::is_same_v<T, double>) {
+            if (precision < 0) {
+                // Shortest representation. Dragonbox is fastest.
+                return value_to_string_dragonbox(value);
+            }
         }
 #endif
 
-#if defined(FMM_TO_CHARS_DOUBLE_SUPPORTED)
+#ifdef FMM_TO_CHARS_DOUBLE_SUPPORTED
         return value_to_string_to_chars(value, precision);
-#elif defined(FMM_USE_RYU)
-        return value_to_string_ryu(value, precision);
 #else
-        return value_to_string_fallback(value, precision);
-#endif
-    }
-
-    /**
-     * long double to string.
-     *
-     * Preference order: to_chars, fallback.
-     * Note: Ryu's generic_128 can do this on some platforms, but it is not reliable.
-     * see https://github.com/ulfjack/ryu/issues/215
-     */
-    inline std::string value_to_string(const long double& value, int precision) {
-#if defined(FMM_TO_CHARS_LONG_DOUBLE_SUPPORTED)
-        return value_to_string_to_chars(value, precision);
+        constexpr bool have_ryu =
+#ifdef FMM_USE_RYU
+            true;
 #else
-        return value_to_string_fallback(value, precision);
+            false;
 #endif
-    }
 
-    inline std::string value_to_string(const std::complex<float>& value, int precision) {
-        return value_to_string(value.real(), precision) + " " + value_to_string(value.imag(), precision);
-    }
-
-    inline std::string value_to_string(const std::complex<double>& value, int precision) {
-        return value_to_string(value.real(), precision) + " " + value_to_string(value.imag(), precision);
+        if constexpr (have_ryu && (std::is_same_v<T, float> || std::is_same_v<T, double>)) {
+            return value_to_string_ryu(value, precision);
+        } else {
+            return value_to_string_fallback(value, precision);
+        }
+#endif
     }
 
-    inline std::string value_to_string(const std::complex<long double>& value, int precision) {
+    template <typename COMPLEX, typename std::enable_if<is_complex<COMPLEX>::value, int>::type = 0>
+    inline std::string value_to_string(const COMPLEX& value, int precision) {
         return value_to_string(value.real(), precision) + " " + value_to_string(value.imag(), precision);
     }
 
     /**
      * Catchall
      */
-    template <typename T>
+    template <typename T, typename std::enable_if<!std::is_integral_v<T> && !std::is_floating_point_v<T> && !is_complex<T>::value, int>::type = 0>
     std::string value_to_string(const T& value, int precision) {
         return value_to_string_fallback(value, precision);
     }


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b7a3618..23d701b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -6,16 +6,28 @@ on:
 
 jobs:
   cpptests:
-    name: C++ Tests on ${{ matrix.os }}
+    name: C++ Tests on ${{ matrix.os }} ${{ matrix.description }}
     runs-on: ${{ matrix.os }}
     strategy:
       fail-fast: false
       matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
+        include:
+          - os: windows-latest
+          - os: ubuntu-latest
+          - os: ubuntu-latest
+            gccver: "13"
+            description: "GCC 13"
+          - os: macos-latest
 
     steps:
     - uses: actions/checkout@v4
 
+    - name: Set up GCC
+      uses: egor-tensin/setup-gcc@v1
+      if: ${{ matrix.gccver != '' }}
+      with:
+        version: ${{ matrix.gccver }}
+
     - name: Install Integrations (Ubuntu)
       if: contains(matrix.os, 'ubuntu')
       run: sudo apt-get install -y libarmadillo-dev
@@ -39,7 +51,6 @@ jobs:
       run: brew install suite-sparse
 
     - name: Configure
-      if: ${{ !contains(matrix.python-version, 'pypy') }}
       run: |
         cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DFAST_MATRIX_MARKET_TEST=ON -DFAST_MATRIX_MARKET_TEST_COVERAGE=ON ${{ env.FMM_CMAKE_FLAGS }}
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ed38391..1f64c3e 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -28,14 +28,19 @@ set(MATRIX_DIR "${CMAKE_CURRENT_LIST_DIR}/matrices/")
 message("Test Matrix directory: ${MATRIX_DIR}")
 add_definitions(-DTEST_MATRIX_DIR="${MATRIX_DIR}")
 
+include(GoogleTest)
+
 add_executable(chunking_test chunking_test.cpp)
 target_link_libraries(chunking_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+gtest_discover_tests(chunking_test)
 
 add_executable(field_conv_test field_conv_test.cpp)
 target_link_libraries(field_conv_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+gtest_discover_tests(field_conv_test)
 
 add_executable(basic_test basic_test.cpp)
 target_link_libraries(basic_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+gtest_discover_tests(basic_test)
 
 add_executable(disabled_features_test disabled_features_test.cpp)
 target_link_libraries(disabled_features_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
@@ -43,29 +48,36 @@ target_compile_definitions(disabled_features_test PUBLIC FMM_NO_VECTOR)
 
 add_executable(user_type_test user_type_test.cpp)
 target_link_libraries(user_type_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+gtest_discover_tests(user_type_test)
 
 add_executable(array_test array_test.cpp)
 target_link_libraries(array_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+gtest_discover_tests(array_test)
 
 add_executable(csc_test csc_test.cpp)
 target_link_libraries(csc_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+gtest_discover_tests(csc_test)
 
 add_executable(triplet_test triplet_test.cpp)
 target_link_libraries(triplet_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+gtest_discover_tests(triplet_test)
 
 add_executable(cxsparse_test cxsparse_test.cpp fake_cxsparse/cs.hpp)
 target_link_libraries(cxsparse_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
-
-include(GoogleTest)
-gtest_discover_tests(chunking_test)
-gtest_discover_tests(field_conv_test)
-gtest_discover_tests(basic_test)
-gtest_discover_tests(user_type_test)
-gtest_discover_tests(array_test)
-gtest_discover_tests(triplet_test)
-gtest_discover_tests(csc_test)
 gtest_discover_tests(cxsparse_test)
 
+if ("cxx_std_23" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
+    include(stdfloat_tests.cmake)
+
+    if (have_stdfloat)
+        message("Including test for C++23 fixed-width floating point types")
+        add_executable(cpp23_test cpp23_test.cpp)
+        target_link_libraries(cpp23_test GTest::gtest_main fast_matrix_market::fast_matrix_market)
+        target_compile_features(cpp23_test PUBLIC cxx_std_23)
+        gtest_discover_tests(cpp23_test)
+    endif()
+endif()
+
 
 option(FAST_MATRIX_MARKET_TEST_EXTERNAL_APPS "Enable tests for bindings (e.g. Blaze, Eigen, etc.)" ON)
 if (FAST_MATRIX_MARKET_TEST_EXTERNAL_APPS)
diff --git a/tests/cpp23_test.cpp b/tests/cpp23_test.cpp
new file mode 100644
index 0000000..9f5488e
--- /dev/null
+++ b/tests/cpp23_test.cpp
@@ -0,0 +1,72 @@
+// Copyright (C) 2023 Adam Lugowski. All rights reserved.
+// Use of this source code is governed by the BSD 2-clause license found in the LICENSE.txt file.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <algorithm>
+#include <fstream>
+#include <stdfloat>
+
+#include "fmm_tests.hpp"
+
+#if defined(__clang__)
+// for TYPED_TEST_SUITE
+#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments"
+#endif
+
+template <typename TRIPLET>
+void read_triplet_file(const std::string& matrix_filename, TRIPLET& triplet, fast_matrix_market::read_options options = {}) {
+    std::ifstream f(kTestMatrixDir + "/" + matrix_filename);
+    options.chunk_size_bytes = 1;
+
+    fast_matrix_market::read_matrix_market_triplet(f, triplet.nrows, triplet.ncols, triplet.rows, triplet.cols, triplet.vals, options);
+}
+
+template <typename TRIPLET>
+std::string write_triplet_string(TRIPLET& triplet, fast_matrix_market::write_options options = {}) {
+    std::ostringstream f;
+    options.chunk_size_values = 1;
+
+    fast_matrix_market::write_matrix_market_triplet(f, {triplet.nrows, triplet.ncols}, triplet.rows, triplet.cols, triplet.vals, options);
+    return f.str();
+}
+
+template <typename VT>
+std::string eye_cycle() {
+    triplet_matrix<int64_t, VT> eye3;
+    read_triplet_file("eye3.mtx", eye3);
+    return write_triplet_string(eye3);
+}
+
+TEST(Cpp23FixedWidthFloats, Eye) {
+    std::string expected = eye_cycle<double>();
+    std::string expected_complex = eye_cycle<std::complex<double>>();
+
+    // test pattern on floats
+    EXPECT_EQ(expected, eye_cycle<float>());
+    EXPECT_EQ(expected_complex, eye_cycle<std::complex<float>>());
+
+#if __STDCPP_FLOAT16_T__
+    EXPECT_EQ(expected, eye_cycle<std::float16_t>());
+    EXPECT_EQ(expected_complex, eye_cycle<std::complex<std::float16_t>>());
+#endif
+
+#if __STDCPP_FLOAT32_T__
+    EXPECT_EQ(expected, eye_cycle<std::float32_t>());
+    EXPECT_EQ(expected_complex, eye_cycle<std::complex<std::float32_t>>());
+#endif
+
+#if __STDCPP_FLOAT64_T__
+    EXPECT_EQ(expected, eye_cycle<std::float64_t>());
+    EXPECT_EQ(expected_complex, eye_cycle<std::complex<std::float64_t>>());
+#endif
+
+#if 0 && __STDCPP_FLOAT128_T__
+    // stdlib support for float128_t is incomplete. Missing <charconv> and stream overloads.
+    EXPECT_EQ(expected, eye_cycle<std::float128_t>());
+    EXPECT_EQ(expected_complex, eye_cycle<std::complex<std::float128_t>>());
+#endif
+
+#if __STDCPP_BFLOAT16_T__
+    EXPECT_EQ(expected, eye_cycle<std::bfloat16_t>());
+#endif
+}
diff --git a/tests/stdfloat_tests.cmake b/tests/stdfloat_tests.cmake
new file mode 100644
index 0000000..006a4dd
--- /dev/null
+++ b/tests/stdfloat_tests.cmake
@@ -0,0 +1,17 @@
+# Test for C++23 fixed-width floating point support.
+include(CheckSourceCompiles)
+
+# CMP0067: Honor language standard in try_compile() source-file signature.
+# https://cmake.org/cmake/help/latest/policy/CMP0067.html
+cmake_policy(SET CMP0067 NEW)
+set(CMAKE_CXX_STANDARD 23)
+
+# Check for header
+check_source_compiles(CXX "
+#include <stdfloat>
+int main(void) {
+    return 0;
+}
+" have_stdfloat)
+
+set(CMAKE_CXX_STANDARD 17)