diff --git a/CMakeLists.txt b/CMakeLists.txt index 6847265..88f743b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.10) -set(version 0.0.3) +set(version 0.1.0) project(numbers VERSION ${version} LANGUAGES CXX) diff --git a/README.md b/README.md index 2fbde25..78a79ad 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,6 @@ numbers [![CMake CI Matrix](https://github.com/guuzaa/numbers/actions/workflows/cmake.yml/badge.svg?branch=main)](https://github.com/guuzaa/numbers/actions/workflows/cmake.yml) ![language c++17](https://img.shields.io/badge/Language-C++17-red) [![license mit](https://img.shields.io/badge/License-MIT-pink)](https://github.com/guuzaa/numbers/blob/main/LICENSE.txt) -![linux](https://img.shields.io/badge/OS-Linux-blue) -![macOS](https://img.shields.io/badge/OS-macOS-blue) -![windows](https://img.shields.io/badge/OS-Windows-blue) - -> [!IMPORTANT] -> This project is in the early stages of development. The codebase is subject to significant changes and reorganization. Expect breaking changes as we refine the architecture, fix bugs, and implement new features. `numbers` is a library for C++17 and later versions that handles integer overflow similar to Rust. It simplifies integer overflow situations. @@ -18,11 +12,14 @@ numbers - **Full Control** over handling integer overflow +- **Support for Multiple Toolchains**: GCC, Clang, MSVC + - Same as **Primitive Types** (WIP) -- **Support Integers**: i8, i16, i32, i64, u8, u16, u32, u64, even i128 & u128 (not ready yet) +- **Support for Various Integer Type**: i8, i16, i32, i64, u8, u16, u32, u64, even i128 & u128 -## Usage +
+Usage ### operator + ```c++ @@ -69,10 +66,11 @@ std::cout << "a= " << a << ", b= " << b << '\n'; numbers::i64 ret = a.saturating_mul(b); std::cout << ret << '\n'; ``` +
## Contribute -We welcome contributions, but please be aware that the project's design and conventions are still evolving. If you'd like to contribute, it's a good idea to discuss your plans with the project maintainers before starting work. +If you'd like to contribute, it's a good idea to discuss your plans with the project maintainers before starting work. For the latest updates and discussions, please see our [issues](https://github.com/guuzaa/numbers/issues) and [pull requests](https://github.com/guuzaa/numbers/pulls). diff --git a/examples/conversion.cc b/examples/conversion.cc new file mode 100644 index 0000000..7ae8ab2 --- /dev/null +++ b/examples/conversion.cc @@ -0,0 +1,19 @@ +#include +#include "numbers.h" + +int main(int argc, char const *argv[]) { + std::cout << "==== conversion example ==== \n"; + numbers::u128 a = 35.6; + numbers::i128 b = 35.6; + numbers::i64 c = 135.6; + std::cout << "a = " << a << ", b = " << b << ", c = " << c << '\n'; + // do some conversions + a = static_cast(c); + b = static_cast(c); + std::cout << "a = " << a << ", b = " << b << ", c = " << c << '\n'; + + b = 345; + c = static_cast(b); + std::cout << "a = " << a << ", b = " << b << ", c = " << c << '\n'; + return 0; +} diff --git a/examples/overflowing-sub.cc b/examples/overflowing_sub.cc similarity index 100% rename from examples/overflowing-sub.cc rename to examples/overflowing_sub.cc diff --git a/src/include/int128.hh b/src/include/int128.hh index 17528d3..8d6c10d 100644 --- a/src/include/int128.hh +++ b/src/include/int128.hh @@ -259,7 +259,6 @@ class int128 { constexpr explicit operator unsigned __int128() const; #endif - // TODO unimplemented! explicit operator float() const; explicit operator double() const; explicit operator long double() const; @@ -724,8 +723,6 @@ inline uint128 operator*(uint128 lhs, uint128 rhs) { #endif } -// TODO implement division and modulo - // Increment/decrement operators inline uint128 uint128::operator++(int) { uint128 tmp(*this); diff --git a/src/include/int128_no_intrinstic.inc b/src/include/int128_no_intrinstic.inc index 46dc8d6..f60ddd2 100644 --- a/src/include/int128_no_intrinstic.inc +++ b/src/include/int128_no_intrinstic.inc @@ -46,6 +46,29 @@ constexpr int128::operator long long() const { return int128_internal::BitCastTo constexpr int128::operator unsigned long long() const { return static_cast(lo_); } +inline int128::operator float() const { + // We must convert the absolute value and then negate as needed, because + // floating point types are typically sign-magnitude. Otherwise, the + // difference between the high and low 64 bits when interpreted as two's + // complement overwhelms the precision of the mantissa. + // + // Also check to make sure we don't negate Int128Min() + return hi_ < 0 && *this != MIN ? -static_cast(-*this) + : static_cast(lo_) + std::ldexp(static_cast(hi_), 64); +} + +inline int128::operator double() const { + // See comment in int128::operator float() above. + return hi_ < 0 && *this != MIN ? -static_cast(-*this) + : static_cast(lo_) + std::ldexp(static_cast(hi_), 64); +} + +inline int128::operator long double() const { + // See comment in int128::operator float() above. + return hi_ < 0 && *this != MIN ? -static_cast(-*this) + : static_cast(lo_) + std::ldexp(static_cast(hi_), 64); +} + // Comparison operators constexpr bool operator==(int128 lhs, int128 rhs) { return (int128_low64(lhs) == int128_low64(rhs) && int128_high64(lhs) == int128_high64(rhs)); diff --git a/src/include/integer.hh b/src/include/integer.hh index 7b1e484..f42c6eb 100644 --- a/src/include/integer.hh +++ b/src/include/integer.hh @@ -28,7 +28,11 @@ class Integer { constexpr Integer() noexcept : num_{} {} template >> - Integer(U num) noexcept : num_{static_cast(num)} {} + Integer(U num) : num_{static_cast(num)} {} + + Integer(float num) noexcept : num_{static_cast(num)} {} + Integer(double num) noexcept : num_{static_cast(num)} {} + Integer(long double num) noexcept : num_{static_cast(num)} {} constexpr Integer operator+(Integer other) const noexcept(false) { if (add_overflow(num_, other.num_)) { diff --git a/src/include/uinteger.hh b/src/include/uinteger.hh index cc423ca..291baa4 100644 --- a/src/include/uinteger.hh +++ b/src/include/uinteger.hh @@ -30,6 +30,10 @@ class Uinteger { template >> Uinteger(U num) noexcept : num_{static_cast(num)} {} + Uinteger(float num) noexcept : num_{static_cast(num)} {} + Uinteger(double num) noexcept : num_{static_cast(num)} {} + Uinteger(long double num) noexcept : num_{static_cast(num)} {} + constexpr Uinteger operator+(const Uinteger &other) const noexcept(false) { if (add_overflow(num_, other.num_)) { throw std::runtime_error("add overflow"); diff --git a/src/numbers/int128.cc b/src/numbers/int128.cc index 01dbd8c..df4bcbf 100644 --- a/src/numbers/int128.cc +++ b/src/numbers/int128.cc @@ -111,8 +111,26 @@ std::string uint128_to_formatted_string(uint128 v, std::ios_base::fmtflags flags return os.str(); } +template +uint128 make_uint128_from_float(T v) { + static_assert(std::is_floating_point::value, ""); + // Undefined behavior if v is NaN or cannot fit into uint128 + assert(std::isfinite(v) && v > -1 && + (std::numeric_limits::max_exponent <= 128 || v < std::ldexp(static_cast(1), 128))); + + if (v >= std::ldexp(static_cast(1), 64)) { + uint64_t hi = static_cast(std::ldexp(v, -64)); + uint64_t lo = static_cast(v - std::ldexp(static_cast(hi), 64)); + return make_uint128(hi, lo); + } + return make_uint128(0, static_cast(v)); +} } // namespace +uint128::uint128(float v) : uint128(make_uint128_from_float(v)) {} +uint128::uint128(double v) : uint128(make_uint128_from_float(v)) {} +uint128::uint128(long double v) : uint128(make_uint128_from_float(v)) {} + } // namespace numbers namespace numbers { @@ -236,4 +254,25 @@ std::ostream &operator<<(std::ostream &os, int128 v) { std::string int128::to_string() const { return uint128_to_formatted_string(*this, std::ios_base::dec); } +#ifndef NUMBERS_HAVE_INTRINSTIC_INT128 +namespace { + +template +int128 make_int128_from_float(T v) { + // Conversion when v is NaN or cannot fit into int128 would be undefined + // behavior if using an intrinsic 128-bit integer. + assert(std::isfinite(v) && (std::numeric_limits::max_exponent <= 127 || + (v >= -std::ldexp(static_cast(1), 127) && v < std::ldexp(static_cast(1), 127)))); + uint128 result = v < 0 ? -make_uint128_from_float(-v) : make_uint128_from_float(v); + return make_int128(int128_internal::BitCastToSigned(uint128_high64(result)), uint128_low64(result)); +} + +} // namespace + +int128::int128(float v) : int128(make_int128_from_float(v)) {} +int128::int128(double v) : int128(make_int128_from_float(v)) {} +int128::int128(long double v) : int128(make_int128_from_float(v)) {} + +#endif + } // namespace numbers \ No newline at end of file diff --git a/tests/integer/int128.test.cc b/tests/integer/int128.test.cc index 2afdce7..73af748 100644 --- a/tests/integer/int128.test.cc +++ b/tests/integer/int128.test.cc @@ -47,6 +47,77 @@ TYPED_TEST(Int128TraitsTest, ConstructAssignTest) { "TypeParam must not be assignable from numbers::int128"); } +typedef ::testing::Types FloatTypes; + +template +class Int128FloatConversionTest : public ::testing::Test {}; + +TYPED_TEST_SUITE(Int128FloatConversionTest, FloatTypes); + +TYPED_TEST(Int128FloatConversionTest, ConstructAndCastTest) { + // Conversions where the floating point values should be exactly the same. + for (int i = 0; i < 110; ++i) { + SCOPED_TRACE(::testing::Message() << "i = " << i); + TypeParam float_value = std::ldexp(static_cast(0x9f4b), i); + int128 int_value = int128(0x9f4b) << i; + + EXPECT_EQ(float_value, static_cast(int_value)); + EXPECT_EQ(-float_value, static_cast(-int_value)); + EXPECT_EQ(int_value, int128(float_value)); + EXPECT_EQ(-int_value, int128(-float_value)); + } + + // Round trip conversions with a small sample of randomly generated uint64_t + // values (less than int64_t max so that value * 2^64 fits into int128). + uint64_t values[] = {0x6d4493c24fb86199, 0x26ecd65e4cb359b5, 0x2c43417433ba3fd1, 0x3b573ec669df6b55, + 0x1c751e55a29f4f0f}; + for (uint64_t value : values) { + for (int i = 0; i <= 64; ++i) { + SCOPED_TRACE(::testing::Message() << "value = " << value << "; i = " << i); + + TypeParam fvalue = std::ldexp(static_cast(value), i); + EXPECT_DOUBLE_EQ(fvalue, static_cast(int128(fvalue))); + EXPECT_DOUBLE_EQ(-fvalue, static_cast(-int128(fvalue))); + EXPECT_DOUBLE_EQ(-fvalue, static_cast(int128(-fvalue))); + EXPECT_DOUBLE_EQ(fvalue, static_cast(-int128(-fvalue))); + } + } + + // Round trip conversions with a small sample of random large positive values. + int128 large_values[] = { + make_int128(0x5b0640d96c7b3d9f, 0xb7a7089e51d18622), make_int128(0x34bed042c6f65270, 0x74b236570669a089), + make_int128(0x43debc9e6da12724, 0xf7f0f83da686797d), make_int128(0x71e8d483be4e5589, 0x75c3f96fb00752b6)}; + for (int128 value : large_values) { + // Make value have as many significant bits as can be represented by + // the mantissa, also making sure the highest and lowest bit in the range + // are set. + value >>= (127 - std::numeric_limits::digits); + value |= int128(1) << (std::numeric_limits::digits - 1); + value |= 1; + for (int i = 0; i < 127 - std::numeric_limits::digits; ++i) { + int128 int_value = value << i; + EXPECT_EQ(int_value, static_cast(static_cast(int_value))); + EXPECT_EQ(-int_value, static_cast(static_cast(-int_value))); + } + } + + // Small sample of checks that rounding is toward zero + EXPECT_EQ(0, int128(TypeParam(0.01))); + EXPECT_EQ(17, int128(TypeParam(17.8))); + EXPECT_EQ(0, int128(TypeParam(-0.803))); + EXPECT_EQ(-53, int128(TypeParam(-53.1))); + EXPECT_EQ(0, int128(TypeParam(0.5))); + EXPECT_EQ(0, int128(TypeParam(-0.05))); + TypeParam just_lt_one = std::nexttoward(TypeParam(1), TypeParam(0)); + EXPECT_EQ(0, int128(just_lt_one)); + TypeParam just_gt_minus_one = std::nexttoward(TypeParam(-1), TypeParam(0)); + EXPECT_EQ(0, int128(just_gt_minus_one)); + + // Check limits + EXPECT_DOUBLE_EQ(std::ldexp(static_cast(1), 127), static_cast(int128_max())); + EXPECT_DOUBLE_EQ(-std::ldexp(static_cast(1), 127), static_cast(int128_min())); +} + TEST(Int128Test, BoolConversion) { EXPECT_FALSE(int128(0)); for (int i = 0; i < 64; ++i) {