diff --git a/doc/modules/ROOT/pages/literals.adoc b/doc/modules/ROOT/pages/literals.adoc index d3b72b2d..2670613c 100644 --- a/doc/modules/ROOT/pages/literals.adoc +++ b/doc/modules/ROOT/pages/literals.adoc @@ -45,7 +45,30 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator ""_I128(const char* str, st The macros at the end allow you to write out a 128-bit number like you would with say `UINT64_C` without having to add quotes. -NOTE: The minimum `int128_t` value cannot be written as a negated `_i128` literal, because its positive magnitude (2^127) exceeds the maximum `int128_t` value and so fails to parse (yielding `0`). Use `BOOST_INT128_INT128_C(-170141183460469231731687303715884105728)` or `(std::numeric_limits::min)()` instead. +== Bases + +The literals accept the standard C++ integer base prefixes in addition to plain decimal. +A `0x` or `0X` prefix selects hexadecimal, `0b` or `0B` selects binary, and a leading `0` selects octal. +The prefix is stripped and the remaining digits are parsed in the selected base. +Digit separators (`'`) may be used in any base. + +[source, c++] +---- +0xDEAD'BEEF_u128 // hexadecimal, == 3735928559 +0b1111'0000_u128 // binary, == 240 +0777_u128 // octal, == 511 +255_u128 // decimal, == 255 +-0xFF_i128 // == -255 (the unary minus is applied after parsing) +"-0b1010"_i128 // == -10 (the string form may embed the sign) +---- + +== Error Handling + +An invalid literal is rejected at compile time rather than being silently accepted. +This includes a value that overflows the target type, a digit that is not valid for the selected base (for example `0b2` or `09`), and an empty magnitude such as `0x`. +When such a literal is used in a constant expression the parse reaches `BOOST_INT128_UNREACHABLE`, which is not a constant expression and therefore is a hard compile error. + +NOTE: The minimum `int128_t` value cannot be written as a negated `_i128` literal, because its positive magnitude (2^127) exceeds the maximum `int128_t` value and so fails to parse (a hard compile error in a constant expression). Use `BOOST_INT128_INT128_C(-170141183460469231731687303715884105728)`, the string form `"-170141183460469231731687303715884105728"_i128`, or `(std::numeric_limits::min)()` instead. See the xref:examples.adoc#examples_construction[construction examples] for usage demonstrations of both literals and macros. diff --git a/examples/to_string.cpp b/examples/to_string.cpp index 1da26c73..68bb4637 100644 --- a/examples/to_string.cpp +++ b/examples/to_string.cpp @@ -55,10 +55,10 @@ int main() std::cout << "Match: " << (s_large_str == s_large_std) << std::endl; // Values beyond 64-bit range. - // The positive magnitude 2^127 exceeds INT128_MAX, so INT128_MIN cannot be - // written as a negated _i128 literal; this line therefore yields 0. Use the - // INT128_C macro (below) or std::numeric_limits::min() instead. - const auto large_negative {-170141183460469231731687303715884105728_i128}; + // INT128_MIN via a negated numeric literal fails: the sign is not part of the + // literal, so the positive magnitude 2^127 is read first and rejected as out of + // range. A string literal keeps the sign with the digits, so it parses cleanly. + const auto large_negative {"-170141183460469231731687303715884105728"_i128}; std::cout << "\nint128_t min with string literal: " << to_string(large_negative) << std::endl; const auto large_negative_c {BOOST_INT128_INT128_C(-170141183460469231731687303715884105728)}; diff --git a/include/boost/int128/detail/mini_from_chars.hpp b/include/boost/int128/detail/mini_from_chars.hpp index 0db8ac56..94291e54 100644 --- a/include/boost/int128/detail/mini_from_chars.hpp +++ b/include/boost/int128/detail/mini_from_chars.hpp @@ -15,6 +15,10 @@ #include #include +#if !(defined(BOOST_INT128_HAS_GPU_SUPPORT) || defined(BOOST_INT128_DISABLE_EXCEPTIONS)) +#include +#endif + #endif namespace boost { @@ -265,8 +269,123 @@ BOOST_INT128_TEST_EXPORT BOOST_INT128_HOST_DEVICE constexpr int from_chars_liter return impl::from_chars_integer_impl(first, last, value, base); } +// Rejects an out of range literal +[[noreturn]] BOOST_INT128_HOST_DEVICE inline void parse_literal_out_of_range() +{ + #if defined(BOOST_INT128_HAS_GPU_SUPPORT) || defined(BOOST_INT128_DISABLE_EXCEPTIONS) + BOOST_INT128_UNREACHABLE; + #else + BOOST_INT128_THROW_EXCEPTION(std::out_of_range("Literal is out of range of the target type")); + #endif +} + +// Rejects an invalid literal +[[noreturn]] BOOST_INT128_HOST_DEVICE inline void parse_invalid_literal() +{ + #if defined(BOOST_INT128_HAS_GPU_SUPPORT) || defined(BOOST_INT128_DISABLE_EXCEPTIONS) + BOOST_INT128_UNREACHABLE; + #else + BOOST_INT128_THROW_EXCEPTION(std::invalid_argument("Literal is not a valid integer")); + #endif +} + +// GCC before 6 rejects a constexpr function that contains a throw-expression or a +// call to a non-constexpr function anywhere in its body so we need to use unreachable in that case +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 6 +# define BOOST_INT128_REJECT_LITERAL(reporter) BOOST_INT128_UNREACHABLE +#else +# define BOOST_INT128_REJECT_LITERAL(reporter) reporter() +#endif + +// Parse a user-defined literal, hard-failing on any malformed or out-of-range input. +// A C++ base prefix (0x/0X hex, 0b/0B binary, or a leading 0 for octal) is stripped and +// the digits parsed in that base, otherwise handled as base 10 +template +BOOST_INT128_HOST_DEVICE constexpr Integer parse_literal(const char* first, const char* last) noexcept +{ + Integer value {}; + + // A leading sign stays with the digits; a base prefix, if present, follows it. + auto next = first; + const bool negative {next != last && *next == '-'}; + if (negative) + { + ++next; + } + + int base {10}; + bool prefixed {false}; + + if (last - next >= 2 && *next == '0') + { + const char marker {next[1]}; + if (marker == 'x' || marker == 'X') + { + base = 16; + next += 2; + prefixed = true; + } + else if (marker == 'b' || marker == 'B') + { + base = 2; + next += 2; + prefixed = true; + } + else + { + base = 8; + next += 1; + prefixed = true; + } + } + + // With no prefix, from_chars_literal handles the sign and the full decimal range. + // Overflow is reported as EDOM; anything else short of full consumption is malformed. + if (!prefixed) + { + const auto status = from_chars_literal(first, last, value); + if (status == EDOM) + { + BOOST_INT128_REJECT_LITERAL(parse_literal_out_of_range); + } + else if (status != first - last) + { + BOOST_INT128_REJECT_LITERAL(parse_invalid_literal); + } + + return value; + } + + // Prefixed: parse the magnitude in the detected base, then reapply the sign. + const auto status = from_chars_literal(next, last, value, base); + if (status == EDOM) + { + BOOST_INT128_REJECT_LITERAL(parse_literal_out_of_range); + } + else if (status != next - last) + { + BOOST_INT128_REJECT_LITERAL(parse_invalid_literal); + } + + if (negative) + { + BOOST_INT128_IF_CONSTEXPR (std::numeric_limits::is_signed) + { + value = static_cast(-value); + } + else + { + BOOST_INT128_UNREACHABLE; + } + } + + return value; +} + } // namespace detail } // namespace int128 } // namespace boost +#undef BOOST_INT128_REJECT_LITERAL + #endif //MINI_FROM_CHARS_HPP diff --git a/include/boost/int128/literals.hpp b/include/boost/int128/literals.hpp index e170f7db..47b18d8a 100644 --- a/include/boost/int128/literals.hpp +++ b/include/boost/int128/literals.hpp @@ -17,58 +17,42 @@ namespace literals { BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr uint128_t operator ""_u128(const char* str) noexcept { - uint128_t result {}; - detail::from_chars_literal(str, str + detail::strlen(str), result); - return result; + return detail::parse_literal(str, str + detail::strlen(str)); } BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr uint128_t operator ""_U128(const char* str) noexcept { - uint128_t result {}; - detail::from_chars_literal(str, str + detail::strlen(str), result); - return result; + return detail::parse_literal(str, str + detail::strlen(str)); } BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr uint128_t operator ""_u128(const char* str, std::size_t len) noexcept { - uint128_t result {}; - detail::from_chars_literal(str, str + len, result); - return result; + return detail::parse_literal(str, str + len); } BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr uint128_t operator ""_U128(const char* str, std::size_t len) noexcept { - uint128_t result {}; - detail::from_chars_literal(str, str + len, result); - return result; + return detail::parse_literal(str, str + len); } BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr int128_t operator ""_i128(const char* str) noexcept { - int128_t result {}; - detail::from_chars_literal(str, str + detail::strlen(str), result); - return result; + return detail::parse_literal(str, str + detail::strlen(str)); } BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr int128_t operator ""_I128(const char* str) noexcept { - int128_t result {}; - detail::from_chars_literal(str, str + detail::strlen(str), result); - return result; + return detail::parse_literal(str, str + detail::strlen(str)); } BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr int128_t operator ""_i128(const char* str, std::size_t len) noexcept { - int128_t result {}; - detail::from_chars_literal(str, str + len, result); - return result; + return detail::parse_literal(str, str + len); } BOOST_INT128_EXPORT BOOST_INT128_HOST_DEVICE constexpr int128_t operator ""_I128(const char* str, std::size_t len) noexcept { - int128_t result {}; - detail::from_chars_literal(str, str + len, result); - return result; + return detail::parse_literal(str, str + len); } } // namespace literals diff --git a/test/Jamfile b/test/Jamfile index f38218ca..e1a2c71c 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -152,3 +152,7 @@ compile compile_tests/numeric_compile.cpp ; compile compile_tests/string_compile.cpp ; compile compile_tests/random_compile.cpp ; compile compile_tests/utilities_compile.cpp ; + +compile-fail compile_tests/literals_overflow_fail.cpp ; +compile-fail compile_tests/literals_base_prefix_fail.cpp ; +compile-fail compile_tests/literals_invalid_fail.cpp ; diff --git a/test/compile_tests/literals_base_prefix_fail.cpp b/test/compile_tests/literals_base_prefix_fail.cpp new file mode 100644 index 00000000..f7e10b0c --- /dev/null +++ b/test/compile_tests/literals_base_prefix_fail.cpp @@ -0,0 +1,19 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// A prefixed user-defined literal whose value does not fit the target type must be +// rejected at compile time. 0x1 followed by 32 zeros is 2^128, one past the +// uint128_t maximum, so the base-16 parse overflows and this must not compile. + +#include + +using namespace boost::int128::literals; + +int main() +{ + constexpr auto value = 0x100000000000000000000000000000000_u128; + static_cast(value); + + return 0; +} diff --git a/test/compile_tests/literals_invalid_fail.cpp b/test/compile_tests/literals_invalid_fail.cpp new file mode 100644 index 00000000..5c12e9dc --- /dev/null +++ b/test/compile_tests/literals_invalid_fail.cpp @@ -0,0 +1,19 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// A user-defined literal whose characters are not valid digits for the base must be +// rejected at compile time instead of being silently truncated. "12xyz" parses the +// leading 12 and then hits the invalid 'x', so the literal is malformed. + +#include + +using namespace boost::int128::literals; + +int main() +{ + constexpr auto value = "12xyz"_u128; + static_cast(value); + + return 0; +} diff --git a/test/compile_tests/literals_overflow_fail.cpp b/test/compile_tests/literals_overflow_fail.cpp new file mode 100644 index 00000000..6679f106 --- /dev/null +++ b/test/compile_tests/literals_overflow_fail.cpp @@ -0,0 +1,18 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// A user-defined literal whose value does not fit the target type must be rejected +// at compile time instead of being silently accepted. + +#include + +using namespace boost::int128::literals; + +int main() +{ + constexpr auto value = 340282366920938463463374607431768211456_u128; + static_cast(value); + + return 0; +} diff --git a/test/test_literals.cpp b/test/test_literals.cpp index fe55ad83..5fade8b7 100644 --- a/test/test_literals.cpp +++ b/test/test_literals.cpp @@ -56,8 +56,43 @@ void test_u128_digit_separators() const boost::int128::uint128_t max_val {std::numeric_limits::max()}; BOOST_TEST(max_val == 340'282'366'920'938'463'463'374'607'431'768'211'455_u128); + // MSVC 14.1 ICE + #if !defined(_MSC_VER) || _MSC_VER >= 1920 // Separators must be usable in a constant expression static_assert(100'000_u128 == boost::int128::uint128_t{100000}, "constexpr separator"); + #endif +} + +void test_u128_base_prefixes() +{ + // A C++ base prefix is recognized and the digits parsed in that base + + // Hexadecimal (0x / 0X) + BOOST_TEST(boost::int128::uint128_t{255} == 0xFF_u128); + BOOST_TEST(boost::int128::uint128_t{255} == 0XfF_U128); + BOOST_TEST(boost::int128::uint128_t{255} == "0xff"_u128); + BOOST_TEST(boost::int128::uint128_t{0xDEADBEEFULL} == 0xDEAD'BEEF_u128); + + // Binary (0b / 0B) + BOOST_TEST(boost::int128::uint128_t{10} == 0b1010_u128); + BOOST_TEST(boost::int128::uint128_t{240} == 0B1111'0000_U128); + + // Octal (leading 0) + BOOST_TEST(boost::int128::uint128_t{511} == 0777_u128); + BOOST_TEST(boost::int128::uint128_t{8} == 010_u128); + BOOST_TEST(boost::int128::uint128_t{0} == 00_u128); + + // A full-width value written in hexadecimal is the maximum + const boost::int128::uint128_t max_val {std::numeric_limits::max()}; + BOOST_TEST(max_val == 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_u128); + + // MSVC 14.1 ICE + #if !defined(_MSC_VER) || _MSC_VER >= 1920 + // Prefixes must be usable in a constant expression + static_assert(0x10_u128 == boost::int128::uint128_t{16}, "constexpr hex"); + static_assert(0b100_u128 == boost::int128::uint128_t{4}, "constexpr binary"); + static_assert(010_u128 == boost::int128::uint128_t{8}, "constexpr octal"); + #endif } void test_i128_literals() @@ -97,8 +132,38 @@ void test_i128_digit_separators() const boost::int128::int128_t max_val {std::numeric_limits::max()}; BOOST_TEST(max_val == 170'141'183'460'469'231'731'687'303'715'884'105'727_i128); + // MSVC 14.1 ICE + #if !defined(_MSC_VER) || _MSC_VER >= 1920 // Separators must be usable in a constant expression static_assert(100'000_i128 == boost::int128::int128_t{100000}, "constexpr separator"); + #endif +} + +void test_i128_base_prefixes() +{ + // Hexadecimal, binary and octal prefixes are recognized for the signed type too + BOOST_TEST(boost::int128::int128_t{127} == 0x7F_i128); + BOOST_TEST(boost::int128::int128_t{255} == "0xFF"_I128); + BOOST_TEST(boost::int128::int128_t{10} == 0b1010_i128); + BOOST_TEST(boost::int128::int128_t{511} == 0777_i128); + + // A leading unary minus is applied after the (prefixed) literal is parsed + BOOST_TEST(boost::int128::int128_t{-255} == -0xFF_i128); + BOOST_TEST(boost::int128::int128_t{-511} == -0777_i128); + + // The string form may embed the sign ahead of the prefix + BOOST_TEST(boost::int128::int128_t{-255} == "-0xFF"_i128); + BOOST_TEST(boost::int128::int128_t{-10} == "-0b1010"_I128); + BOOST_TEST(boost::int128::int128_t{-511} == "-0777"_i128); + + // A full-width positive value written in hexadecimal is the maximum + const boost::int128::int128_t max_val {std::numeric_limits::max()}; + BOOST_TEST(max_val == 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF_i128); + + // MSVC 14.1 ICE + #if !defined(_MSC_VER) || _MSC_VER >= 1920 + static_assert(-0x10_i128 == boost::int128::int128_t{-16}, "constexpr signed hex"); + #endif } #ifdef _MSC_VER @@ -109,8 +174,10 @@ int main() { test_u128_literals(); test_u128_digit_separators(); + test_u128_base_prefixes(); test_i128_literals(); test_i128_digit_separators(); + test_i128_base_prefixes(); return boost::report_errors(); }