Compare commits

...

12 Commits
v8.3.2 ... main

Author SHA1 Message Date
github-actions[bot]
dbeabeeb3e Version 8.3.3 2026-10-31 13:45:33 +00:00
Daniel Lemire
9054aa02be
Merge pull request #430 from CODER-7777/fix-427-unlikely-cxx20-ext-warning
Silence -Wc++20-extensions at the remaining [[unlikely]] sites
2026-10-31 09:42:54 -04:00
Daniel Lemire
33785d5d61
Merge pull request #431 from aikhanjum/codex/fastfloat-correctness
Reject integer_times_pow10 arguments wider than 64 bits
2026-10-29 13:54:48 -04:00
Aikhan Jumashukurov
4b37bda237 Reject integer_times_pow10 arguments wider than 64 bits 2026-10-29 13:00:34 -04:00
CODER-7777
78b6a96f28 Silence -Wc++20-extensions at the remaining [[unlikely]] sites (fixes #427)
The javascript early-exit checks added in b2e41e8 use fastfloat_unlikely
without the point-of-use clang diagnostic guard that the other call sites
have (see #387), so clang with -pedantic in C++11/14/17 mode warns
"use of the 'unlikely' attribute is a C++20 extension".

Wrap both sites with the same guarded pragma push/pop already used in
parse_number.h and decimal_to_binary.h. No change to code generation.

Add a CI step compiling fast_float.h with clang-14 and
-pedantic-errors -Werror in C++11/14/17/20 to catch regressions.
2026-10-29 05:53:02 +05:30
Daniel Lemire
0de7a21da5 Run clang-format 2026-10-17 08:55:50 -04:00
Daniel Lemire
638a8acd6d
Merge pull request #426 from sahvx655-wq/power-negative-shift
Avoid right-shifting a negative value in detail::power()
2026-10-17 08:54:58 -04:00
Sahana Bogar
90513adca1 Avoid right-shifting a negative value in detail::power() 2026-10-15 11:53:39 +05:30
Daniel Lemire
166d2b3df2
Merge pull request #425 from fastfloat/fix-424-fixed-width-invalid
Leave std::float32_t/std::float64_t unmodified on invalid input (fixes #424)
2026-10-12 10:11:38 -04:00
Daniel Lemire
9cd6dfc8e3 Leave std::float32_t/std::float64_t unmodified on invalid input (fixes #424)
The fixed-width wrappers parsed into a zero-initialized temporary and
copied it back unconditionally, so invalid input overwrote the
destination with zero. Copy back only when the result is not
invalid_argument, as required by [charconv.from.chars]/1.
2026-10-12 10:10:54 -04:00
Daniel Lemire
033f64f608
Merge pull request #423 from sahvx655-wq/fuzz-coverage
extend fuzzer to wide char types, integer parsing and all formats
2026-10-11 17:35:37 -04:00
Sahana Bogar
e7e0cfcd4f extend fuzzer to wide char types, integer parsing and all formats 2026-10-12 01:55:39 +05:30
11 changed files with 329 additions and 43 deletions

View File

@ -9,6 +9,14 @@ jobs:
- uses: actions/checkout@v7
- name: Install clang++-14
run: sudo apt-get install -y clang++-14
- name: Check for pedantic warnings in pre-C++20 modes (issue 427)
run: |
set -xe
printf '#include "fast_float/fast_float.h"\nint main() { return 0; }\n' > pedantic_check.cpp
for std in c++11 c++14 c++17 c++20; do
clang++-14 -std=$std -Wall -Wextra -pedantic-errors -Werror \
-Iinclude -fsyntax-only pedantic_check.cpp
done
- name: Use cmake
run: |
mkdir build &&

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.14)
project(fast_float VERSION 8.3.2 LANGUAGES CXX)
project(fast_float VERSION 8.3.3 LANGUAGES CXX)
set(FASTFLOAT_CXX_STANDARD 11 CACHE STRING "the C++ standard to use for fastfloat")
set(CMAKE_CXX_STANDARD ${FASTFLOAT_CXX_STANDARD})
option(FASTFLOAT_TEST "Enable tests" OFF)

View File

@ -2,7 +2,7 @@
module(
name = "fast_float",
version = "8.3.2",
version = "8.3.3",
compatibility_level = 8,
)

View File

@ -482,7 +482,7 @@ float: 12345678 * 10^23 = 1.23456782e+30 (==expected)
```
Overloads of `fast_float::integer_times_pow10()` are provided for
signed and unsigned integer types: `int64_t`, `uint64_t`, etc.
signed and unsigned integer types up to 64 bits: `int64_t`, `uint64_t`, etc.
## Users and Related Work
@ -587,7 +587,7 @@ sufficiently recent version of CMake (3.11 or better at least):
FetchContent_Declare(
fast_float
GIT_REPOSITORY https://github.com/fastfloat/fast_float.git
GIT_TAG tags/v8.3.2
GIT_TAG tags/v8.3.3
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(fast_float)
@ -603,7 +603,7 @@ You may also use [CPM](https://github.com/cpm-cmake/CPM.cmake), like so:
CPMAddPackage(
NAME fast_float
GITHUB_REPOSITORY "fastfloat/fast_float"
GIT_TAG v8.3.2)
GIT_TAG v8.3.3)
```
## Using as single header
@ -615,7 +615,7 @@ if desired as described in the command line help.
You may directly download automatically generated single-header files:
<https://github.com/fastfloat/fast_float/releases/download/v8.3.2/fast_float.h>
<https://github.com/fastfloat/fast_float/releases/download/v8.3.3/fast_float.h>
## Benchmarking

View File

@ -1,38 +1,205 @@
#include "fast_float/fast_float.h"
#include <fuzzer/FuzzedDataProvider.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <system_error>
#include <type_traits>
#include <vector>
#if defined(__has_include)
#if __has_include(<charconv>) && __cplusplus >= 201703L
#include <charconv>
#define FASTFLOAT_FUZZ_CHARCONV 1
#endif
#endif
static void fail(char const *check) {
fprintf(stderr, "fast_float fuzz failure: %s\n", check);
abort();
}
fast_float::chars_format arbitrary_format(FuzzedDataProvider &fdp) {
using fast_float::chars_format;
switch (fdp.ConsumeIntegralInRange<int>(0, 3)) {
chars_format format;
switch (fdp.ConsumeIntegralInRange<int>(0, 7)) {
case 0:
return chars_format::scientific;
format = chars_format::scientific;
break;
case 1:
return chars_format::fixed;
format = chars_format::fixed;
break;
case 2:
return chars_format::fixed;
format = chars_format::json;
break;
case 3:
format = chars_format::json_or_infnan;
break;
case 4:
format = chars_format::fortran;
break;
case 5:
format = chars_format::javascript;
break;
case 6:
format = chars_format::javascript_sloppy;
break;
default:
format = chars_format::general;
break;
}
return chars_format::general;
if (fdp.ConsumeBool()) {
format = format | chars_format::allow_leading_plus;
}
if (fdp.ConsumeBool()) {
format = format | chars_format::skip_white_space;
}
return format;
}
template <typename T> uint64_t bits_of(T value) {
uint64_t bits = 0;
memcpy(&bits, &value, sizeof(T));
return bits;
}
// The same ASCII text must parse identically for every character type: same
// error code, same number of consumed code units, bit-identical value.
template <typename UC>
void check_widened(std::string const &input,
fast_float::from_chars_result_t<char> expected,
double expected_value, fast_float::chars_format format) {
std::vector<UC> wide;
wide.reserve(input.size());
for (char c : input) {
wide.push_back(UC(static_cast<unsigned char>(c)));
}
double value = 0.0;
auto result = fast_float::from_chars(wide.data(), wide.data() + wide.size(),
value, format);
if (result.ec != expected.ec ||
(result.ptr - wide.data()) != (expected.ptr - input.data())) {
fail("wide result disagrees with char result");
}
if (expected.ec == std::errc() && bits_of(value) != bits_of(expected_value)) {
fail("wide value disagrees with char value");
}
}
// Every number format accepted by the library is pure ASCII, so a successful
// parse must never consume a code unit >= 0x80. A violation means some helper
// inspected only the low byte of a wide code unit (the bug behind #391).
// The generated sequences keep an ASCII low byte but take hostile high bits:
// top-bit-set units (near the minimum of a signed wchar_t, the shape behind
// #422), sign-extended negative units (the shape behind #395), and non-ASCII
// code points whose low byte is an ASCII digit (the shape behind #391).
template <typename UC>
void check_arbitrary_units(FuzzedDataProvider &fdp,
fast_float::chars_format format) {
using unsigned_uc = typename std::make_unsigned<UC>::type;
size_t count = fdp.ConsumeIntegralInRange<size_t>(0, 64);
std::vector<UC> units;
units.reserve(count);
for (size_t i = 0; i < count; ++i) {
unsigned_uc unit = fdp.ConsumeIntegral<uint8_t>();
switch (fdp.ConsumeIntegralInRange<int>(0, 4)) {
case 0: // top bit set: near the minimum value when UC is signed
unit = unsigned_uc(unit | (unsigned_uc(1) << (sizeof(UC) * 8 - 1)));
break;
case 1: // all high bits set: a small negative value when UC is signed
unit = unsigned_uc(unit | ~unsigned_uc(0xFF));
break;
case 2: // arbitrary high byte: a non-ASCII code point, ASCII low byte
unit = unsigned_uc(
unit | unsigned_uc(unsigned_uc(fdp.ConsumeIntegral<uint8_t>()) << 8));
break;
default: // plain ASCII unit
break;
}
units.push_back(UC(unit));
}
if (units.empty()) {
return;
}
UC const *begin = units.data();
UC const *end = begin + units.size();
double as_double = 0.0;
auto result = fast_float::from_chars(begin, end, as_double, format);
if (result.ec == std::errc()) {
for (UC const *p = begin; p != result.ptr; ++p) {
if (static_cast<unsigned_uc>(*p) >= 0x80) {
fail("float parse consumed a non-ASCII code unit");
}
}
}
uint16_t as_uint16 = 0;
auto int_result = fast_float::from_chars(begin, end, as_uint16);
if (int_result.ec == std::errc()) {
for (UC const *p = begin; p != int_result.ptr; ++p) {
if (static_cast<unsigned_uc>(*p) >= 0x80) {
fail("integer parse consumed a non-ASCII code unit");
}
}
}
}
// fast_float's integer parsing is specified to match std::from_chars.
template <typename T> void check_int(std::string const &input, int base) {
T value{};
auto result = fast_float::from_chars(
input.data(), input.data() + input.size(), value, base);
#ifdef FASTFLOAT_FUZZ_CHARCONV
T expected{};
auto std_result = std::from_chars(input.data(), input.data() + input.size(),
expected, base);
if (result.ec != std_result.ec || result.ptr != std_result.ptr) {
fail("integer result disagrees with std::from_chars");
}
if (result.ec == std::errc() && value != expected) {
fail("integer value disagrees with std::from_chars");
}
#else
static_cast<void>(result);
#endif
}
template <typename T>
void check_int_all_bases(std::string const &input, int base) {
check_int<T>(input, 10);
check_int<T>(input, base);
}
extern "C" int LLVMFuzzerTestOneInput(uint8_t const *data, size_t size) {
FuzzedDataProvider fdp(data, size);
fast_float::chars_format format = arbitrary_format(fdp);
double result_d = 0.0;
int base = fdp.ConsumeIntegralInRange<int>(2, 36);
std::string input_d = fdp.ConsumeRandomLengthString(128);
auto answer = fast_float::from_chars(
double result_d = 0.0;
auto answer_d = fast_float::from_chars(
input_d.data(), input_d.data() + input_d.size(), result_d, format);
std::string input_f = fdp.ConsumeRandomLengthString(128);
float result_f = 0.0;
answer = fast_float::from_chars(
input_f.data(), input_f.data() + input_f.size(), result_f, format);
int result_i = 0;
fast_float::from_chars(input_d.data(), input_d.data() + input_d.size(),
result_f, format);
check_widened<wchar_t>(input_d, answer_d, result_d, format);
check_widened<char16_t>(input_d, answer_d, result_d, format);
check_widened<char32_t>(input_d, answer_d, result_d, format);
std::string input_i = fdp.ConsumeRandomLengthString(128);
answer = fast_float::from_chars(input_i.data(),
input_i.data() + input_i.size(), result_i);
check_int_all_bases<uint8_t>(input_i, base);
check_int_all_bases<int8_t>(input_i, base);
check_int_all_bases<uint16_t>(input_i, base);
check_int_all_bases<int16_t>(input_i, base);
check_int_all_bases<uint32_t>(input_i, base);
check_int_all_bases<int32_t>(input_i, base);
check_int_all_bases<uint64_t>(input_i, base);
check_int_all_bases<int64_t>(input_i, base);
check_arbitrary_units<wchar_t>(fdp, format);
check_arbitrary_units<char16_t>(fdp, format);
check_arbitrary_units<char32_t>(fdp, format);
return 0;
}

View File

@ -622,10 +622,22 @@ parse_number_string(UC const *p, UC const *pend, parse_options_t<UC> options,
// JSON and JavaScript are mutually exclusive, so only the non-JSON
// instantiation has to look at the flag.
FASTFLOAT_IF_CONSTEXPR17(!basic_json_fmt) {
// We have to disable -Wc++20-extensions for the [[unlikely]] attribute
// See comment for @jwakely at
// https://github.com/fastfloat/fast_float/pull/387#discussion_r3366943539
#ifdef __clang__
#pragma clang diagnostic push
#if (!defined(__APPLE_CC__) && __clang_major__ >= 10) || (__clang_major__ >= 13)
#pragma clang diagnostic ignored "-Wc++20-extensions"
#endif
#endif
if fastfloat_unlikely (uint64_t(options.format &
detail::basic_javascript_fmt)) {
return parse_number_string_javascript<UC>(p, pend, options, store_spans);
}
#ifdef __clang__
#pragma clang diagnostic pop
#endif
}
return parse_number_string_impl<basic_json_fmt, false, UC>(p, pend, options,
store_spans);

View File

@ -61,9 +61,15 @@ namespace detail {
* -ceil(p) + q
* where
* p = log(5**-q)/log(2) = -q * log(5)/log(2)
*
* The shift must floor towards negative infinity, as an arithmetic right
* shift does, but right-shifting a negative value is implementation-defined
* before C++20. Biasing the product by 2048 * 65536 keeps the shifted value
* non-negative without changing the result: the bias is a multiple of 2^16
* and is removed again after the shift.
*/
constexpr fastfloat_really_inline int32_t power(int32_t q) noexcept {
return (((152170 + 65536) * q) >> 16) + 63;
return (((152170 + 65536) * q + (2048 * 65536)) >> 16) - 2048 + 63;
}
} // namespace detail

View File

@ -18,7 +18,7 @@
#define FASTFLOAT_VERSION_MAJOR 8
#define FASTFLOAT_VERSION_MINOR 3
#define FASTFLOAT_VERSION_PATCH 2
#define FASTFLOAT_VERSION_PATCH 3
#define FASTFLOAT_STRINGIZE_IMPL(x) #x
#define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x)

View File

@ -158,7 +158,10 @@ template <> struct from_chars_caller<std::float32_t> {
// float32_t
float val = 0.0f;
auto ret = from_chars_advanced(first, last, val, options);
// If no characters match the pattern, value must be left unmodified.
if (ret.ec != std::errc::invalid_argument) {
value = val;
}
return ret;
}
};
@ -175,7 +178,10 @@ template <> struct from_chars_caller<std::float64_t> {
// float64_t
double val = 0.0;
auto ret = from_chars_advanced(first, last, val, options);
// If no characters match the pattern, value must be left unmodified.
if (ret.ec != std::errc::invalid_argument) {
value = val;
}
return ret;
}
};
@ -385,9 +391,19 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value,
// Leave for the javascript conversion before anything else is live, so this
// compiles to a tail call and the common path keeps its registers. The cold
// function repeats the leading-whitespace and empty-input handling below.
// See the comment on -Wc++20-extensions further down in this function.
#ifdef __clang__
#pragma clang diagnostic push
#if (!defined(__APPLE_CC__) && __clang_major__ >= 10) || (__clang_major__ >= 13)
#pragma clang diagnostic ignored "-Wc++20-extensions"
#endif
#endif
if fastfloat_unlikely (uint64_t(fmt & detail::basic_javascript_fmt)) {
return from_chars_float_javascript<T, UC>(first, last, value, options);
}
#ifdef __clang__
#pragma clang diagnostic pop
#endif
from_chars_result_t<UC> answer;
if (uint64_t(fmt & chars_format::skip_white_space)) {
@ -529,10 +545,9 @@ integer_times_pow10(int64_t mantissa, int decimal_exponent) noexcept {
// the following overloads are here to avoid surprising ambiguity for int,
// unsigned, etc.
template <typename T, typename Int>
FASTFLOAT_CONSTEXPR20
typename std::enable_if<is_supported_float_type<T>::value &&
std::is_integral<Int>::value &&
!std::is_signed<Int>::value,
FASTFLOAT_CONSTEXPR20 typename std::enable_if<
is_supported_float_type<T>::value && std::is_integral<Int>::value &&
sizeof(Int) <= sizeof(uint64_t) && !std::is_signed<Int>::value,
T>::type
integer_times_pow10(Int mantissa, int decimal_exponent) noexcept {
return integer_times_pow10<T>(static_cast<uint64_t>(mantissa),
@ -540,10 +555,9 @@ FASTFLOAT_CONSTEXPR20
}
template <typename T, typename Int>
FASTFLOAT_CONSTEXPR20
typename std::enable_if<is_supported_float_type<T>::value &&
std::is_integral<Int>::value &&
std::is_signed<Int>::value,
FASTFLOAT_CONSTEXPR20 typename std::enable_if<
is_supported_float_type<T>::value && std::is_integral<Int>::value &&
sizeof(Int) <= sizeof(int64_t) && std::is_signed<Int>::value,
T>::type
integer_times_pow10(Int mantissa, int decimal_exponent) noexcept {
return integer_times_pow10<T>(static_cast<int64_t>(mantissa),
@ -551,15 +565,21 @@ FASTFLOAT_CONSTEXPR20
}
template <typename Int>
FASTFLOAT_CONSTEXPR20 typename std::enable_if<
std::is_integral<Int>::value && !std::is_signed<Int>::value, double>::type
FASTFLOAT_CONSTEXPR20
typename std::enable_if<std::is_integral<Int>::value &&
sizeof(Int) <= sizeof(uint64_t) &&
!std::is_signed<Int>::value,
double>::type
integer_times_pow10(Int mantissa, int decimal_exponent) noexcept {
return integer_times_pow10(static_cast<uint64_t>(mantissa), decimal_exponent);
}
template <typename Int>
FASTFLOAT_CONSTEXPR20 typename std::enable_if<
std::is_integral<Int>::value && std::is_signed<Int>::value, double>::type
FASTFLOAT_CONSTEXPR20
typename std::enable_if<std::is_integral<Int>::value &&
sizeof(Int) <= sizeof(int64_t) &&
std::is_signed<Int>::value,
double>::type
integer_times_pow10(Int mantissa, int decimal_exponent) noexcept {
return integer_times_pow10(static_cast<int64_t>(mantissa), decimal_exponent);
}

View File

@ -2283,6 +2283,38 @@ void verify_integer_times_pow10(Int mantissa, int decimal_exponent) {
}
} // namespace all_supported_types
#if defined(__SIZEOF_INT128__)
template <typename Int, typename = void>
struct has_integer_times_pow10 : std::false_type {};
template <typename Int>
struct has_integer_times_pow10<
Int, std::void_t<decltype(fast_float::integer_times_pow10(Int{}, 0))>>
: std::true_type {};
template <typename Float, typename Int, typename = void>
struct has_typed_integer_times_pow10 : std::false_type {};
template <typename Float, typename Int>
struct has_typed_integer_times_pow10<
Float, Int,
std::void_t<decltype(fast_float::integer_times_pow10<Float>(Int{}, 0))>>
: std::true_type {};
TEST_CASE("integer_times_pow10 rejects integers wider than 64 bits") {
CHECK(has_integer_times_pow10<int64_t>::value);
CHECK(has_integer_times_pow10<uint64_t>::value);
CHECK(has_typed_integer_times_pow10<float, int64_t>::value);
CHECK(has_typed_integer_times_pow10<double, uint64_t>::value);
CHECK_FALSE(has_integer_times_pow10<__int128_t>::value);
CHECK_FALSE(has_integer_times_pow10<__uint128_t>::value);
CHECK_FALSE(has_typed_integer_times_pow10<float, __int128_t>::value);
CHECK_FALSE(has_typed_integer_times_pow10<float, __uint128_t>::value);
CHECK_FALSE(has_typed_integer_times_pow10<double, __int128_t>::value);
CHECK_FALSE(has_typed_integer_times_pow10<double, __uint128_t>::value);
}
#endif
TEST_CASE("integer_times_pow10") {
/* explicitly verifying API with different types of integers */
// double (the "default" overload)

View File

@ -2,6 +2,8 @@
#include <iostream>
#include <vector>
#include <cstring>
#include <limits>
#include <string_view>
#include "fast_float/fast_float.h"
#include <cstdint>
@ -9,6 +11,39 @@
#include <stdfloat>
#endif
// If no characters match the pattern, value must be left unmodified
// (see https://github.com/fastfloat/fast_float/issues/424).
template <typename T> bool check_invalid_input_preserves_value() {
std::vector<std::string_view> const invalid_inputs{"x", "", "-", ".",
"e1", "-x", "na", "+"};
for (auto const &f : invalid_inputs) {
T result = T(42);
auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result);
if (answer.ec != std::errc::invalid_argument || answer.ptr != f.data()) {
std::cerr << "Expected invalid_argument for input: \"" << f << "\""
<< std::endl;
return false;
}
if (result != T(42)) {
std::cerr << "Value modified on invalid input: \"" << f << "\" got "
<< result << std::endl;
return false;
}
}
// Out-of-range input must still store the documented value.
std::string_view const too_large = "1e999";
T result = T(42);
auto answer = fast_float::from_chars(
too_large.data(), too_large.data() + too_large.size(), result);
if (answer.ec != std::errc::result_out_of_range ||
result != std::numeric_limits<T>::infinity()) {
std::cerr << "Expected infinity and result_out_of_range for \"1e999\""
<< std::endl;
return false;
}
return true;
}
int main() {
// Write some testcases for the parsing of floating point numbers in the
// float32_t type. We use the from_chars function defined in this library.
@ -33,6 +68,9 @@ int main() {
return EXIT_FAILURE;
}
}
if (!check_invalid_input_preserves_value<std::float32_t>()) {
return EXIT_FAILURE;
}
#else
std::cout << "No std::float32_t type available." << std::endl;
#endif
@ -60,6 +98,9 @@ int main() {
return EXIT_FAILURE;
}
}
if (!check_invalid_input_preserves_value<std::float64_t>()) {
return EXIT_FAILURE;
}
#else
std::cout << "No std::float64_t type available." << std::endl;
#endif