From c31e0294c3334f16649dabe39dd0550b2a50c5d1 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sat, 26 Sep 2026 14:53:33 -0400 Subject: [PATCH 1/7] CI: pin Alpine to v3.22 (setup-alpine v1 fails with apk-tools v3) --- .github/workflows/alpine.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/alpine.yml b/.github/workflows/alpine.yml index 2e8d988..842313b 100644 --- a/.github/workflows/alpine.yml +++ b/.github/workflows/alpine.yml @@ -24,7 +24,9 @@ jobs: uses: jirutka/setup-alpine@v1 with: arch: ${{ matrix.arch }} - branch: ${{ matrix.arch == 'riscv64' && 'edge' || 'latest-stable' }} + # v3.22 is the last release with apk-tools v2; setup-alpine v1 bootstraps + # with apk.static v2, which cannot set up the apk v3 releases. + branch: v3.22 packages: > build-base cmake From fe5e97eeac55d7a809f8f04a6fa539967effdc19 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sun, 4 Oct 2026 09:34:39 -0400 Subject: [PATCH 2/7] Reorder parsed_number_string_t fields to avoid padding (fixes #418) Placing the parse_error field next to the booleans shrinks the struct from 72 to 64 bytes on 64-bit systems, recovering a large performance regression observed with MSVC. Add a static_assert to guard the size. --- include/fast_float/ascii_number.h | 5 ++++- tests/basictest.cpp | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index e814f12..4d68eaa 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -308,13 +308,16 @@ template struct parsed_number_string_t { int64_t exponent{0}; uint64_t mantissa{0}; UC const *lastmatch{nullptr}; + // The field order matters: placing 'error' next to the booleans avoids + // padding, keeping the struct at 64 bytes on 64-bit systems instead of 72. + // See https://github.com/fastfloat/fast_float/issues/418 + parse_error error{parse_error::no_error}; bool negative{false}; bool valid{false}; bool too_many_digits{false}; // contains the range of the significant digits span integer{}; // non-nullable span fraction{}; // nullable - parse_error error{parse_error::no_error}; }; using byte_span = span; diff --git a/tests/basictest.cpp b/tests/basictest.cpp index 9b45822..cde42bc 100644 --- a/tests/basictest.cpp +++ b/tests/basictest.cpp @@ -84,6 +84,12 @@ char const *round_name(int d) { } } +// Guard against padding regressions that hurt performance. +// See https://github.com/fastfloat/fast_float/issues/418 +static_assert(sizeof(void *) != 8 || + sizeof(fast_float::parsed_number_string_t) <= 64, + "parsed_number_string_t should fit in 64 bytes"); + #define FASTFLOAT_STR(x) #x #define SHOW_DEFINE(x) printf("%s='%s'\n", #x, FASTFLOAT_STR(x)) From e62540e95fbb4640b7c5758d8869be921988a984 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sun, 4 Oct 2026 12:05:40 -0400 Subject: [PATCH 3/7] Add manually triggered release workflow; fix on-release asset upload - New release.yml (workflow_dispatch): bumps patch/minor/major, runs script/release.py, tests the amalgamation and the full test suite, commits and tags on main, and publishes a release with fast_float.h. - on-release.yml: the upload failed with 'Validation Failed' because fast_float.h was already attached to the release. Use 'gh release upload --clobber' instead of the deprecated actions/upload-release-asset, and run the amalgamation test binary. - release.py: also bump MODULE.bazel (stuck at 8.2.4) and exit non-zero if amalgamation fails. --- .github/workflows/on-release.yml | 16 ++--- .github/workflows/release.yml | 104 +++++++++++++++++++++++++++++++ script/release.py | 13 ++++ 3 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml index 7427df4..707424f 100644 --- a/.github/workflows/on-release.yml +++ b/.github/workflows/on-release.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v7 - + - name: Amalgamate fast_float.h run: | mkdir build @@ -29,13 +29,13 @@ jobs: cp tests/string_test.cpp build/ cd build g++ string_test.cpp + ./a.out - name: Upload Release Asset - uses: actions/upload-release-asset@v1 + # --clobber replaces an asset that was already attached by hand, which + # would otherwise make the upload fail. env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: build/fast_float/fast_float.h - asset_name: fast_float.h - asset_content_type: text/plain + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ github.event.release.tag_name }}" \ + build/fast_float/fast_float.h --clobber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..08d3313 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,104 @@ +name: Release + +# Manually triggered. Bumps the version (patch, minor or major), regenerates +# the amalgamated fast_float.h, runs the tests, commits the version bump to +# main, tags it and publishes a GitHub release with fast_float.h attached. +# +# Releases created with GITHUB_TOKEN do not trigger other workflows, so +# on-release.yml does not run for releases made here. + +'on': + workflow_dispatch: + inputs: + bump: + description: 'Which part of the version to bump' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: main + fetch-depth: 0 + + - name: Compute new version + id: version + run: | + current=$(git describe --abbrev=0 --tags) + current=${current#v} + IFS=. read -r major minor patch <<< "$current" + case "${{ inputs.bump }}" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + *) echo "unknown bump type"; exit 1 ;; + esac + new="$major.$minor.$patch" + echo "current=$current" >> "$GITHUB_OUTPUT" + echo "new=$new" >> "$GITHUB_OUTPUT" + echo "Releasing $current -> $new (${{ inputs.bump }})" + + - name: Bump version files and amalgamate + run: | + new="${{ steps.version.outputs.new }}" + IFS=. read -r major minor patch <<< "$new" + python3 script/release.py "$new" + rm -f CMakeLists.txt.bak README.md.bak MODULE.bazel.bak \ + include/fast_float/float_common.h.bak + grep -q "project(fast_float VERSION $new LANGUAGES CXX)" CMakeLists.txt + grep -q "#define FASTFLOAT_VERSION_MAJOR $major$" include/fast_float/float_common.h + grep -q "#define FASTFLOAT_VERSION_MINOR $minor$" include/fast_float/float_common.h + grep -q "#define FASTFLOAT_VERSION_PATCH $patch$" include/fast_float/float_common.h + grep -q "version = \"$new\"," MODULE.bazel + grep -q "releases/download/v$new/fast_float.h" README.md + grep -q "#define FASTFLOAT_VERSION_PATCH $patch$" fast_float.h + + - name: Test amalgamation + run: | + mkdir -p amalgamation/fast_float + cp fast_float.h amalgamation/fast_float/ + g++ -O2 -std=c++17 -Iamalgamation -o amalgamation/string_test tests/string_test.cpp + ./amalgamation/string_test + + - name: Build and test + run: | + cmake -B build -DFASTFLOAT_TEST=ON -DCMAKE_CXX_FLAGS='-Werror -Wundef' + cmake --build build --parallel + ctest --test-dir build --output-on-failure + + - name: Commit, tag and push + run: | + new="${{ steps.version.outputs.new }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CMakeLists.txt README.md MODULE.bazel include/fast_float/float_common.h + git commit -m "Version $new" + git tag -a "v$new" -m "version $new" + git push origin main + git push origin "v$new" + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + new="${{ steps.version.outputs.new }}" + gh release create "v$new" \ + --title "Version $new" \ + --generate-notes \ + --notes-start-tag "v${{ steps.version.outputs.current }}" \ + fast_float.h diff --git a/script/release.py b/script/release.py index 4bcf6c8..eacf6a1 100755 --- a/script/release.py +++ b/script/release.py @@ -145,6 +145,18 @@ for line in fileinput.input(versionfile, inplace=1, backup=".bak"): print(f"{versionfile} modified") +bazelfile = f"{maindir}{os.sep}MODULE.bazel" + +for line in fileinput.input(bazelfile, inplace=1, backup=".bak"): + line = re.sub( + r'^(\s*version = )"\d+\.\d+\.\d+",', + rf'\g<1>"{newversionstring}",', + line.rstrip(), + ) + print(line) + +print(f"modified {bazelfile}, a backup was made") + readmefile = f"{maindir}{os.sep}README.md" for line in fileinput.input(readmefile, inplace=1, backup=".bak"): @@ -173,6 +185,7 @@ with open(f"{maindir}{os.sep}fast_float.h", "w") as outfile: if cp.returncode != 0: print("Failed to run amalgamate") + sys.exit(cp.returncode) else: print("amalgamate.py ran successfully") print(f"You should upload {maindir}{os.sep}fast_float.h") From f3f02c8ad0afd8181166dabce6a9e69f8aec24de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:26:53 +0000 Subject: [PATCH 4/7] Version 8.3.1 --- CMakeLists.txt | 2 +- MODULE.bazel | 2 +- README.md | 6 +++--- include/fast_float/float_common.h | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 308e82f..95601bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.3.0 LANGUAGES CXX) +project(fast_float VERSION 8.3.1 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) diff --git a/MODULE.bazel b/MODULE.bazel index 9f63ac4..ad1dff3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "fast_float", - version = "8.2.4", + version = "8.3.1", compatibility_level = 8, ) diff --git a/README.md b/README.md index 35f62ab..e6d9484 100644 --- a/README.md +++ b/README.md @@ -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.0 + GIT_TAG tags/v8.3.1 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.0) + GIT_TAG v8.3.1) ``` ## 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: - + ## Benchmarking diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 1070603..bf61951 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -18,7 +18,7 @@ #define FASTFLOAT_VERSION_MAJOR 8 #define FASTFLOAT_VERSION_MINOR 3 -#define FASTFLOAT_VERSION_PATCH 0 +#define FASTFLOAT_VERSION_PATCH 1 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) From 127167512976d2e1581f908f2c6dd5c701259048 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Mon, 5 Oct 2026 22:46:40 +0530 Subject: [PATCH 5/7] avoid signed negation overflow in integer_times_pow10 Negating the most negative int64_t is undefined behaviour. GCC at -O2 uses that to turn the fast-path mantissa bound into a signed compare and returns the wrong sign, so take the magnitude in uint64_t instead. --- include/fast_float/parse_number.h | 4 +++- tests/basictest.cpp | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index 038be5b..f8409d0 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -503,7 +503,9 @@ FASTFLOAT_CONSTEXPR20 typename std::enable_if::value, T>::type integer_times_pow10(int64_t mantissa, int decimal_exponent) noexcept { const bool is_negative = mantissa < 0; - const uint64_t m = static_cast(is_negative ? -mantissa : mantissa); + // Negate as unsigned: -mantissa overflows for the most negative int64_t. + const uint64_t m = is_negative ? uint64_t(0) - static_cast(mantissa) + : static_cast(mantissa); T value; if (clinger_fast_path_impl(m, decimal_exponent, is_negative, value)) diff --git a/tests/basictest.cpp b/tests/basictest.cpp index cde42bc..8ae74fc 100644 --- a/tests/basictest.cpp +++ b/tests/basictest.cpp @@ -2485,6 +2485,10 @@ TEST_CASE("integer_times_pow10") { all::verify_integer_times_pow10(12345678901234567890ull, -42); all::verify_integer_times_pow10(std::numeric_limits::max(), 42); all::verify_integer_times_pow10(std::numeric_limits::max(), -42); + // the most negative int64_t has no positive counterpart to negate into + all::verify_integer_times_pow10(std::numeric_limits::min(), 0); + all::verify_integer_times_pow10(std::numeric_limits::min(), 42); + all::verify_integer_times_pow10(std::numeric_limits::min(), -42); all::verify_integer_times_pow10(std::numeric_limits::max(), 42); all::verify_integer_times_pow10(std::numeric_limits::max(), -42); } From 50f01dbb516b3baad44af2e76f1bef8b843e306a Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Thu, 8 Oct 2026 19:12:13 +0530 Subject: [PATCH 6/7] avoid signed overflow in is_integer for negative code units --- include/fast_float/ascii_number.h | 8 +++++++- tests/wide_char_test.cpp | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index 4d68eaa..28d6113 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -32,7 +32,13 @@ template fastfloat_really_inline constexpr bool has_simd_opt() { // able to optimize it well. template fastfloat_really_inline constexpr bool is_integer(UC c) noexcept { - return static_cast(c - UC('0')) <= 9u; + // UC can be signed (wchar_t is a signed int on Linux and macOS), and the + // subtraction is then promoted to int and overflows for code units near the + // bottom of the range. Subtracting in the unsigned type wraps instead, which + // selects the same code units, as ch_to_digit already does. + using UnsignedUC = typename std::make_unsigned::type; + return static_cast(static_cast(c) - + static_cast(UC('0'))) <= 9u; } fastfloat_really_inline constexpr uint64_t byteswap(uint64_t val) { diff --git a/tests/wide_char_test.cpp b/tests/wide_char_test.cpp index f31e1ba..7ba54a2 100644 --- a/tests/wide_char_test.cpp +++ b/tests/wide_char_test.cpp @@ -70,9 +70,34 @@ bool test_non_space_with_space_low_byte() { return true; } +// is_integer subtracts UC('0') from the code unit. When wchar_t is signed the +// subtraction is promoted to int and overflows for code units near the bottom +// of the range, so the sanitized build aborts on input the library is supposed +// to reject. Such a unit is not a digit, so both entry points must fail. +bool test_code_unit_below_zero_digit() { + if (!std::is_signed::value) { + return true; // only reproducible where wchar_t is signed + } + wchar_t const input[] = {(std::numeric_limits::min)(), L'1'}; + double result; + auto answer = fast_float::from_chars(input, input + 2, result); + if (answer.ec != std::errc::invalid_argument) { + std::cerr << "a code unit below L'0' must not start a number\n"; + return false; + } + unsigned long long integer; + auto int_answer = fast_float::from_chars(input, input + 2, integer, 10); + if (int_answer.ec != std::errc::invalid_argument) { + std::cerr << "a code unit below L'0' must not start an integer\n"; + return false; + } + return true; +} + int main() { if (test_minus() && test_plus() && test_space() && test_nan() && - test_non_space_with_space_low_byte()) { + test_non_space_with_space_low_byte() && + test_code_unit_below_zero_digit()) { std::cout << "all ok" << std::endl; return EXIT_SUCCESS; } From 42f254836ee416ffcf2f4e6576260003ac33d48c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:56:38 +0000 Subject: [PATCH 7/7] Version 8.3.2 --- CMakeLists.txt | 2 +- MODULE.bazel | 2 +- README.md | 6 +++--- include/fast_float/float_common.h | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 95601bf..78b350f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.3.1 LANGUAGES CXX) +project(fast_float VERSION 8.3.2 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) diff --git a/MODULE.bazel b/MODULE.bazel index ad1dff3..d821208 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,7 +2,7 @@ module( name = "fast_float", - version = "8.3.1", + version = "8.3.2", compatibility_level = 8, ) diff --git a/README.md b/README.md index e6d9484..0c2f5d5 100644 --- a/README.md +++ b/README.md @@ -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.1 + GIT_TAG tags/v8.3.2 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.1) + GIT_TAG v8.3.2) ``` ## 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: - + ## Benchmarking diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index bf61951..60c28c7 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -18,7 +18,7 @@ #define FASTFLOAT_VERSION_MAJOR 8 #define FASTFLOAT_VERSION_MINOR 3 -#define FASTFLOAT_VERSION_PATCH 1 +#define FASTFLOAT_VERSION_PATCH 2 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x)