From 3644f5137c7876704b70f3287835d6befd8b5841 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Wed, 27 May 2026 12:13:56 -0400 Subject: [PATCH 01/30] Update README.md to remove and modify content Removed references to Redis and Valkey, and updated the description of the fast_float library. --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index be0f1f5..5e44349 100644 --- a/README.md +++ b/README.md @@ -441,7 +441,6 @@ The fast_float library is part of: * [WebKit](https://github.com/WebKit/WebKit), the engine behind Safari (Apple's web browser), * [DuckDB](https://duckdb.org), -* [Redis](https://github.com/redis/redis) and [Valkey](https://github.com/valkey-io/valkey), * [Apache Arrow](https://github.com/apache/arrow/pull/8494) where it multiplied the number parsing speed by two or three times, * [Google Jsonnet](https://github.com/google/jsonnet), @@ -455,15 +454,6 @@ system](https://github.com/SerenityOS/serenity/commit/53b7f5e6a11e663c83df8030c3 has a derived implementation that is inherited by the [Ladybird Browser](https://github.com/LadybirdBrowser/ladybird). -The fast_float library provides a performance similar to that of the -[fast_double_parser](https://github.com/lemire/fast_double_parser) library but -using an updated algorithm reworked from the ground up, and while offering an -API more in line with the expectations of C++ programmers. The -fast_double_parser library is part of the [Microsoft LightGBM machine-learning -framework](https://github.com/microsoft/LightGBM). - - - Packages ------ From baffc5719774068393253316328138c4668550ca Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Wed, 27 May 2026 12:43:38 -0400 Subject: [PATCH 02/30] new site --- .github/workflows/pages.yml | 90 +++++++ docs/.nojekyll | 0 docs/README.md | 25 ++ docs/assets/app.js | 116 +++++++++ docs/assets/logo.svg | 15 ++ docs/assets/style.css | 454 ++++++++++++++++++++++++++++++++++++ docs/index.html | 349 +++++++++++++++++++++++++++ 7 files changed, 1049 insertions(+) create mode 100644 .github/workflows/pages.yml create mode 100644 docs/.nojekyll create mode 100644 docs/README.md create mode 100644 docs/assets/app.js create mode 100644 docs/assets/logo.svg create mode 100644 docs/assets/style.css create mode 100644 docs/index.html diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..f23f429 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,90 @@ +name: Deploy GitHub Pages + +# Builds the docs/ site, substitutes the latest release tag into the HTML, +# then deploys the result to GitHub Pages. Runs on: +# - any push to main that touches docs/, this workflow, or CMakeLists.txt +# - every published release (so a new tag refreshes the site) +# - manual dispatch from the Actions tab +on: + push: + branches: [main] + paths: + - "docs/**" + - ".github/workflows/pages.yml" + - "CMakeLists.txt" + release: + types: [published] + workflow_dispatch: + +# Required permissions for the deploy-pages action. +permissions: + contents: read + pages: write + id-token: write + +# Avoid concurrent deploys; let the latest one win. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + name: Build site + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6.0.2 + with: + # Needed so the git-tag fallback can find release tags. + fetch-depth: 0 + fetch-tags: true + + - name: Resolve latest release version + id: version + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + # Prefer the latest published GitHub release. + tag="$(gh release view --repo ${{ github.repository }} --json tagName --jq .tagName 2>/dev/null || true)" + if [ -z "${tag:-}" ]; then + # Fall back to the newest semver-looking git tag. + tag="$(git tag --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" + fi + if [ -z "${tag:-}" ]; then + # Last-resort fallback: read version from CMakeLists.txt. + tag="v$(grep -E 'project\(fast_float VERSION' CMakeLists.txt | sed -E 's/.*VERSION ([0-9.]+).*/\1/')" + fi + # Strip a leading "v" — the template uses bare semver after "v". + version="${tag#v}" + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "Resolved version: ${version} (tag ${tag})" + + - name: Substitute version into HTML + run: | + set -euo pipefail + version='${{ steps.version.outputs.version }}' + # In-place replace every {{VERSION}} occurrence under docs/. + find docs -type f \( -name '*.html' -o -name '*.md' -o -name '*.css' -o -name '*.js' \) \ + -exec sed -i "s/{{VERSION}}/${version}/g" {} + + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..640a9cb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,25 @@ +# fast_float website + +The source for . + +Static HTML, CSS, and a small JS file — no build step required. To preview +locally, serve this directory with any static file server, for example: + +```bash +python3 -m http.server -d docs 8080 +# then open http://localhost:8080/ +``` + +## How the version number stays current + +The displayed release version is kept fresh by two independent mechanisms: + +1. **Build-time substitution.** The `Deploy GitHub Pages` workflow + (`.github/workflows/pages.yml`) replaces every occurrence of the literal + `{{VERSION}}` in `index.html` with the latest GitHub release tag before + publishing. The workflow runs on every push to `main` that touches + `docs/**`, on every published release, and can be dispatched manually. + +2. **Client-side refresh.** `assets/app.js` queries the GitHub Releases API + on page load and overwrites any element marked with `data-version`. This + means visitors see the very latest tag even between deploys. diff --git a/docs/assets/app.js b/docs/assets/app.js new file mode 100644 index 0000000..241a7a8 --- /dev/null +++ b/docs/assets/app.js @@ -0,0 +1,116 @@ +(function () { + "use strict"; + + // ---------- Theme toggle ---------- + const root = document.documentElement; + const storedTheme = localStorage.getItem("ff-theme"); + if (storedTheme === "light" || storedTheme === "dark") { + root.setAttribute("data-theme", storedTheme); + } + const toggle = document.getElementById("theme-toggle"); + if (toggle) { + toggle.addEventListener("click", function () { + const current = + root.getAttribute("data-theme") || + (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"); + const next = current === "dark" ? "light" : "dark"; + root.setAttribute("data-theme", next); + localStorage.setItem("ff-theme", next); + }); + } + + // ---------- Copy-to-clipboard on code blocks ---------- + document.querySelectorAll("pre > code").forEach(function (code) { + const pre = code.parentElement; + if (!pre || pre.querySelector(".copy-btn")) return; + const btn = document.createElement("button"); + btn.className = "copy-btn"; + btn.type = "button"; + btn.textContent = "Copy"; + btn.setAttribute("aria-label", "Copy code to clipboard"); + btn.addEventListener("click", function () { + const text = code.innerText; + const done = function () { + btn.textContent = "Copied"; + btn.classList.add("copied"); + setTimeout(function () { + btn.textContent = "Copy"; + btn.classList.remove("copied"); + }, 1400); + }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(done, function () { + fallbackCopy(text); + done(); + }); + } else { + fallbackCopy(text); + done(); + } + }); + pre.appendChild(btn); + }); + + function fallbackCopy(text) { + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand("copy"); } catch (e) { /* ignore */ } + document.body.removeChild(ta); + } + + // ---------- Live version refresh from GitHub Releases ---------- + // The build-time workflow substitutes {{VERSION}} in the HTML; this + // additionally refreshes the displayed version in the browser so the + // site always reflects the very latest release without redeploying. + const versionNodes = document.querySelectorAll("[data-version]"); + const downloadLink = document.getElementById("download-latest"); + if (versionNodes.length === 0) return; + + // Don't refetch more than once per hour per visitor. + const CACHE_KEY = "ff-latest-release"; + const CACHE_TTL = 60 * 60 * 1000; + let cached = null; + try { + const raw = localStorage.getItem(CACHE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) { + cached = parsed; + } + } + } catch (e) { /* ignore */ } + + if (cached && cached.tag) { + applyVersion(cached.tag, cached.url); + } else { + fetch("https://api.github.com/repos/fastfloat/fast_float/releases/latest", { + headers: { Accept: "application/vnd.github+json" }, + }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (data) { + if (!data || !data.tag_name) return; + try { + localStorage.setItem( + CACHE_KEY, + JSON.stringify({ ts: Date.now(), tag: data.tag_name, url: data.html_url }) + ); + } catch (e) { /* ignore */ } + applyVersion(data.tag_name, data.html_url); + }) + .catch(function () { /* offline / rate limited — keep build-time value */ }); + } + + function applyVersion(tag, url) { + const clean = tag.replace(/^v/, ""); + versionNodes.forEach(function (el) { + // Preserve a leading "v" if the original text used one. + const wasV = (el.textContent || "").trim().startsWith("v"); + el.textContent = (wasV ? "v" : "") + clean; + }); + if (downloadLink && url) downloadLink.href = url; + } +})(); diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..35043fc --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/docs/assets/style.css b/docs/assets/style.css new file mode 100644 index 0000000..c57c40b --- /dev/null +++ b/docs/assets/style.css @@ -0,0 +1,454 @@ +:root { + --bg: #ffffff; + --bg-alt: #f7f8fb; + --surface: #ffffff; + --text: #0f172a; + --text-muted: #475569; + --border: #e2e8f0; + --accent: #2563eb; + --accent-strong: #1d4ed8; + --accent-soft: rgba(37, 99, 235, 0.10); + --grad-from: #2563eb; + --grad-to: #0ea5e9; + --code-bg: #0f172a; + --code-text: #e2e8f0; + --shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.06); + --radius: 12px; + --radius-sm: 8px; + --maxw: 1100px; +} + +:root[data-theme="dark"] { + --bg: #0b1020; + --bg-alt: #0f172a; + --surface: #121a2e; + --text: #e6edf7; + --text-muted: #94a3b8; + --border: #1f2a44; + --accent: #60a5fa; + --accent-strong: #93c5fd; + --accent-soft: rgba(96, 165, 250, 0.14); + --grad-from: #60a5fa; + --grad-to: #22d3ee; + --code-bg: #060a17; + --code-text: #e6edf7; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #0b1020; + --bg-alt: #0f172a; + --surface: #121a2e; + --text: #e6edf7; + --text-muted: #94a3b8; + --border: #1f2a44; + --accent: #60a5fa; + --accent-strong: #93c5fd; + --accent-soft: rgba(96, 165, 250, 0.14); + --grad-from: #60a5fa; + --grad-to: #22d3ee; + --code-bg: #060a17; + --code-text: #e6edf7; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); + } +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; + font-size: 16px; + line-height: 1.6; + color: var(--text); + background: var(--bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +code, pre { + font-family: "SF Mono", Menlo, Consolas, "Liberation Mono", "Roboto Mono", monospace; + font-size: 0.92em; +} +:not(pre) > code { + background: var(--accent-soft); + color: var(--accent-strong); + padding: 0.12em 0.4em; + border-radius: 4px; + font-size: 0.88em; +} + +pre { + background: var(--code-bg); + color: var(--code-text); + padding: 1.1rem 1.2rem; + border-radius: var(--radius); + overflow-x: auto; + margin: 1rem 0 1.5rem; + position: relative; + box-shadow: var(--shadow); + font-size: 0.88rem; + line-height: 1.55; +} +pre code { background: transparent; color: inherit; padding: 0; } +/* highlight.js applies its own background — keep our pre styling */ +pre code.hljs { background: transparent; padding: 0; } + +.copy-btn { + position: absolute; + top: 8px; + right: 8px; + background: rgba(255, 255, 255, 0.08); + color: var(--code-text); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + padding: 4px 10px; + font-size: 0.75rem; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease, background 0.15s ease; +} +pre:hover .copy-btn { opacity: 1; } +.copy-btn:hover { background: rgba(255, 255, 255, 0.15); } +.copy-btn.copied { background: rgba(34, 197, 94, 0.25); border-color: rgba(34, 197, 94, 0.5); } + +.container { + max-width: var(--maxw); + margin: 0 auto; + padding: 0 1.25rem; +} + +.skip { + position: absolute; left: -9999px; + background: var(--accent); color: white; + padding: 0.6rem 1rem; border-radius: 0 0 6px 0; +} +.skip:focus { left: 0; top: 0; } + +/* Header */ +.site-header { + position: sticky; + top: 0; + z-index: 50; + backdrop-filter: saturate(180%) blur(12px); + -webkit-backdrop-filter: saturate(180%) blur(12px); + background: color-mix(in srgb, var(--bg) 80%, transparent); + border-bottom: 1px solid var(--border); +} +.nav { + display: flex; + align-items: center; + gap: 1.25rem; + height: 60px; +} +.brand { + display: inline-flex; + align-items: center; + gap: 0.55rem; + font-weight: 700; + color: var(--text); + text-decoration: none; +} +.brand img { display: block; } +.primary-nav { + display: flex; + gap: 1.2rem; + margin-left: 1rem; + flex: 1; +} +.primary-nav a { + color: var(--text-muted); + font-size: 0.92rem; + font-weight: 500; +} +.primary-nav a:hover { color: var(--text); text-decoration: none; } +.nav-actions { display: flex; align-items: center; gap: 0.5rem; } + +.ghost-btn { + display: inline-flex; align-items: center; gap: 0.4rem; + padding: 0.45rem 0.8rem; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 0.9rem; + background: var(--surface); + transition: border-color 0.15s, background 0.15s; +} +.ghost-btn:hover { border-color: var(--accent); text-decoration: none; } + +.theme-toggle { + width: 36px; height: 36px; + display: inline-flex; align-items: center; justify-content: center; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + color: var(--text); + cursor: pointer; +} +.theme-toggle .i-moon { display: none; } +.theme-toggle .i-sun { display: block; } +:root[data-theme="dark"] .theme-toggle .i-sun, +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) .theme-toggle .i-sun { display: none; } + :root:not([data-theme="light"]) .theme-toggle .i-moon { display: block; } +} +:root[data-theme="dark"] .theme-toggle .i-sun { display: none; } +:root[data-theme="dark"] .theme-toggle .i-moon { display: block; } +:root[data-theme="light"] .theme-toggle .i-sun { display: block; } +:root[data-theme="light"] .theme-toggle .i-moon { display: none; } + +/* Hero */ +.hero { + padding: 5rem 0 4rem; + background: + radial-gradient(1200px 480px at 50% -10%, var(--accent-soft), transparent 70%), + var(--bg); + border-bottom: 1px solid var(--border); +} +.eyebrow { + display: inline-block; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--accent); + background: var(--accent-soft); + padding: 0.3rem 0.7rem; + border-radius: 999px; + margin-bottom: 1.25rem; +} +.hero h1 { + font-size: clamp(2rem, 4.6vw, 3.6rem); + line-height: 1.1; + margin: 0 0 1.25rem; + font-weight: 800; + letter-spacing: -0.02em; +} +.grad { + background: linear-gradient(90deg, var(--grad-from), var(--grad-to)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.lede { + font-size: 1.15rem; + color: var(--text-muted); + max-width: 720px; + margin: 0 0 2rem; +} +.lede strong { color: var(--text); } + +.cta-row { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-bottom: 1.75rem; } + +.btn { + display: inline-flex; align-items: center; gap: 0.5rem; + padding: 0.7rem 1.1rem; + border-radius: 10px; + font-weight: 600; + font-size: 0.95rem; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + cursor: pointer; + transition: transform 0.05s ease, border-color 0.15s, background 0.15s; +} +.btn:hover { text-decoration: none; border-color: var(--accent); } +.btn:active { transform: translateY(1px); } +.btn.primary { + background: var(--accent); + border-color: var(--accent); + color: white; + box-shadow: 0 6px 20px rgba(37, 99, 235, 0.25); +} +.btn.primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); } +.btn.ghost { background: transparent; } +.btn code { background: rgba(255, 255, 255, 0.18); color: inherit; padding: 0.1em 0.45em; border-radius: 4px; } +.btn:not(.primary) code { background: var(--accent-soft); color: var(--accent-strong); } + +.hero-meta { + display: flex; + flex-wrap: wrap; + gap: 0.6rem 1rem; + align-items: center; + font-size: 0.9rem; + color: var(--text-muted); +} +.badge { + display: inline-flex; align-items: center; gap: 0.5rem; + padding: 0.3rem 0.7rem; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface); +} +.badge code { background: transparent; color: var(--text); padding: 0; } +.badge .dot { + width: 8px; height: 8px; border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.18); +} +.meta-link { color: var(--text-muted); } +.meta-link:hover { color: var(--accent); } + +/* Sections */ +.section { padding: 4.5rem 0; } +.section.alt { background: var(--bg-alt); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); } +.section h2 { + font-size: clamp(1.6rem, 3vw, 2.2rem); + margin: 0 0 0.5rem; + letter-spacing: -0.01em; +} +.section h3 { margin: 1.75rem 0 0.6rem; font-size: 1.1rem; } +.section-lede { color: var(--text-muted); max-width: 720px; margin: 0 0 2rem; } + +/* Grid cards */ +.grid { display: grid; gap: 1rem; } +.grid.features { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } +.grid.install { grid-template-columns: 1fr; } + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.4rem 1.4rem 1.5rem; + box-shadow: var(--shadow); + transition: transform 0.12s ease, border-color 0.15s; +} +.card:hover { transform: translateY(-2px); border-color: var(--accent); } +.card h3 { margin: 0 0 0.5rem; font-size: 1.05rem; } +.card p, .card ul { margin: 0; color: var(--text-muted); font-size: 0.95rem; } +.card pre { margin: 0.6rem 0 0; font-size: 0.82rem; } + +.bullets { padding-left: 1.1rem; } +.bullets li { margin: 0.2rem 0; } + +/* Benchmarks */ +.bench { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; + box-shadow: var(--shadow); +} +.bench-row { + display: grid; + grid-template-columns: 160px 1fr; + align-items: center; + gap: 0.9rem; + margin: 0.55rem 0; +} +.bench-label { font-weight: 600; color: var(--text); font-size: 0.95rem; } +.bench-bar { + background: var(--bg-alt); + border-radius: 6px; + overflow: hidden; + height: 28px; + position: relative; +} +.bench-fill { + display: inline-flex; + align-items: center; + height: 100%; + width: var(--w, 0%); + background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 35%, transparent), color-mix(in srgb, var(--accent) 55%, transparent)); + color: var(--text); + padding: 0 0.7rem; + border-radius: 6px; + font-variant-numeric: tabular-nums; + font-size: 0.85rem; + font-weight: 600; + white-space: nowrap; +} +.bench-fill.primary { + background: linear-gradient(90deg, var(--grad-from), var(--grad-to)); + color: white; +} + +@media (max-width: 600px) { + .bench-row { grid-template-columns: 1fr; gap: 0.3rem; } +} + +.repro { + margin-top: 1.25rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + padding: 0.4rem 1rem; +} +.repro summary { + cursor: pointer; + padding: 0.5rem 0; + font-weight: 600; + color: var(--text-muted); +} +.repro[open] summary { color: var(--text); } +.repro pre { margin: 0.5rem 0 0.75rem; } + +/* Users list */ +.users { + list-style: none; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.6rem 1rem; +} +.users li { + padding: 0.7rem 0.9rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + font-size: 0.92rem; + color: var(--text-muted); +} +.users li strong { color: var(--text); margin-right: 0.35rem; } +.aside { color: var(--text-muted); margin-top: 1.5rem; font-size: 0.92rem; } + +.papers { list-style: none; padding: 0; } +.papers li { + padding: 1rem 1.2rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--surface); + margin-bottom: 0.7rem; +} + +/* Footer */ +.site-footer { + background: var(--bg-alt); + border-top: 1px solid var(--border); + padding: 3rem 0 2rem; + color: var(--text-muted); + font-size: 0.92rem; + margin-top: 2rem; +} +.footer-grid { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr; + gap: 2rem; + margin-bottom: 2rem; +} +.site-footer h4 { color: var(--text); font-size: 0.95rem; margin: 0 0 0.6rem; } +.site-footer ul { list-style: none; padding: 0; margin: 0; } +.site-footer li { margin: 0.25rem 0; } +.subfoot { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1.5rem; + border-top: 1px solid var(--border); + flex-wrap: wrap; + gap: 0.5rem; +} +.subfoot code { background: var(--surface); color: var(--text); border: 1px solid var(--border); } + +@media (max-width: 720px) { + .primary-nav { display: none; } + .footer-grid { grid-template-columns: 1fr; gap: 1.5rem; } + .hero { padding: 3rem 0 2.5rem; } + .section { padding: 3rem 0; } +} diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..fd2d17d --- /dev/null +++ b/docs/index.html @@ -0,0 +1,349 @@ + + + + + + fast_float — parse floating-point numbers at a gigabyte per second + + + + + + + + + + + + + + + + + + +
+
+
+ C++11 · header-only · triple-licensed +

Parse floating-point numbers at a gigabyte per second.

+

+ fast_float is a header-only C++ implementation of + std::from_chars for float, double, and integer types. + Exact IEEE rounding, no allocations, no exceptions — often many times faster + than your standard library. +

+ +
+ + Latest release: v{{VERSION}} + + Apache 2.0 + MIT + Boost +
+
+
+ +
+
+

Why fast_float?

+

A drop-in from_chars built for performance-critical code paths.

+
+
+

Blazing fast

+

Often 4× faster than the best competitor and many times faster than typical standard-library implementations. Sustains 1 GB/s on commodity hardware.

+
+
+

Exact rounding

+

Returns the closest IEEE 754 float or double with round-to-nearest, ties-to-even — bit-for-bit correct.

+
+
+

Header-only

+

Just drop in fast_float.h or use it via CMake, Conan, vcpkg, xmake, or Homebrew. Requires only C++11.

+
+
+

No surprises

+

Does not allocate, does not throw, locale-independent. The interface mirrors C++17 std::from_chars.

+
+
+

Integers too

+

Parses every standard integer type in bases 2–36, plus bool. The same fast, allocation-free interface.

+
+
+

Unicode & formats

+

UTF-8, UTF-16, and UTF-32 inputs. JSON, Fortran, and custom decimal separators via from_chars_advanced.

+
+
+

constexpr-ready

+

In C++20, parse strings at compile time with consteval — zero runtime cost.

+
+
+

Portable

+

Visual Studio, GCC, Clang, MSYS2. Linux, macOS, FreeBSD, Windows. x86-64, ARM, RISC-V, s390x. 32-bit and 64-bit.

+
+
+
+
+ +
+
+

Performance

+

+ Parsing random floating-point numbers, measured in megabytes per second + (higher is better). Source: project benchmark suite on a typical x86-64 box. +

+ +
+
+ fast_float +
1042 MB/s
+
+
+ abseil +
430 MB/s
+
+
+ netlib +
271 MB/s
+
+
+ double-conversion +
225 MB/s
+
+
+ strtod +
191 MB/s
+
+
+ +
+ Reproduce these numbers +
cmake -B build -D FASTFLOAT_BENCHMARKS=ON
+cmake --build build
+./build/benchmarks/benchmark
+
+
+
+ +
+
+

Quick start

+

Parse a double from a string in three lines.

+ +
#include "fast_float/fast_float.h"
+#include <iostream>
+#include <string>
+
+int main() {
+  std::string input = "3.1416 xyz ";
+  double result;
+  auto answer = fast_float::from_chars(input.data(),
+                                       input.data() + input.size(),
+                                       result);
+  if (answer.ec != std::errc()) {
+    std::cerr << "parsing failure\n";
+    return EXIT_FAILURE;
+  }
+  std::cout << "parsed the number " << result << '\n';
+}
+ +

Integers in any base (2–36)

+
uint64_t value;
+std::string hex = "4f0cedc95a718c";
+auto r = fast_float::from_chars(hex.data(), hex.data() + hex.size(), value, 16);
+// value == 22250738585072012
+ +

UTF-16 input

+
std::u16string input = u"3.1416 xyz ";
+double result;
+auto r = fast_float::from_chars(input.data(), input.data() + input.size(), result);
+ +

Comma as decimal separator, Fortran, or JSON

+
// "3,1416" — French-style
+fast_float::parse_options opts{fast_float::chars_format::general, ','};
+fast_float::from_chars_advanced(s.data(), s.data() + s.size(), result, opts);
+
+// "1d+4" — Fortran exponent
+opts = {fast_float::chars_format::fortran};
+
+// strict JSON per RFC 8259
+opts = {fast_float::chars_format::json};
+ +

C++20: parse at compile time

+
consteval double parse(std::string_view s) {
+  double v;
+  auto r = fast_float::from_chars(s.data(), s.data() + s.size(), v);
+  return r.ec == std::errc() ? v : -1.0;
+}
+constexpr double pi = parse("3.1415");  // computed at compile time
+
+
+ +
+
+

Install

+

Pick the workflow that matches your project.

+ +
+
+

CMake FetchContent

+
FetchContent_Declare(
+  fast_float
+  GIT_REPOSITORY https://github.com/fastfloat/fast_float.git
+  GIT_TAG tags/v{{VERSION}}
+  GIT_SHALLOW TRUE)
+FetchContent_MakeAvailable(fast_float)
+target_link_libraries(myprogram PUBLIC fast_float)
+
+ +
+

CPM

+
CPMAddPackage(
+  NAME fast_float
+  GITHUB_REPOSITORY "fastfloat/fast_float"
+  GIT_TAG v{{VERSION}})
+
+ +
+

Single header

+

Download a pre-amalgamated header — no build system required.

+
curl -LO https://github.com/fastfloat/fast_float/releases/download/v{{VERSION}}/fast_float.h
+
+ + +
+
+
+ +
+
+

Trusted by

+

fast_float ships inside compilers, browsers, databases, and more.

+
    +
  • GCC — backs std::from_chars since version 12
  • +
  • Chromium — Chrome, Edge, and Opera
  • +
  • WebKit — Safari
  • +
  • Ladybird — independent browser engine
  • +
  • DuckDB — in-process analytical database
  • +
  • Apache Arrow — 2–3× faster number parsing
  • +
  • ClickHouse — OLAP database
  • +
  • MySQL
  • +
  • Boost.JSON
  • +
  • Blender
  • +
  • Google Jsonnet
  • +
+

+ Ports and bindings exist for + Rust, + Java, + C#, + C, and + R. +

+
+
+ +
+
+

The research behind it

+ +
+
+
+ + + + + + + + + + From 7790aa6231b8f81bef1f31e48b5ff06f65a14403 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Wed, 27 May 2026 13:11:22 -0400 Subject: [PATCH 03/30] gh pages --- .github/workflows/pages.yml | 52 ++++++++++--------------------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index f23f429..2bbc472 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,10 +1,5 @@ name: Deploy GitHub Pages -# Builds the docs/ site, substitutes the latest release tag into the HTML, -# then deploys the result to GitHub Pages. Runs on: -# - any push to main that touches docs/, this workflow, or CMakeLists.txt -# - every published release (so a new tag refreshes the site) -# - manual dispatch from the Actions tab on: push: branches: [main] @@ -16,26 +11,23 @@ on: types: [published] workflow_dispatch: -# Required permissions for the deploy-pages action. permissions: - contents: read - pages: write - id-token: write + contents: write # Changed: need write to push to gh-pages + pages: read # No longer needed for deploy-pages + id-token: none -# Avoid concurrent deploys; let the latest one win. concurrency: group: "pages" cancel-in-progress: false jobs: - build: - name: Build site + deploy: + name: Deploy to gh-pages runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: - # Needed so the git-tag fallback can find release tags. fetch-depth: 0 fetch-tags: true @@ -45,46 +37,28 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - # Prefer the latest published GitHub release. tag="$(gh release view --repo ${{ github.repository }} --json tagName --jq .tagName 2>/dev/null || true)" if [ -z "${tag:-}" ]; then - # Fall back to the newest semver-looking git tag. tag="$(git tag --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" fi if [ -z "${tag:-}" ]; then - # Last-resort fallback: read version from CMakeLists.txt. tag="v$(grep -E 'project\(fast_float VERSION' CMakeLists.txt | sed -E 's/.*VERSION ([0-9.]+).*/\1/')" fi - # Strip a leading "v" — the template uses bare semver after "v". version="${tag#v}" echo "tag=${tag}" >> "$GITHUB_OUTPUT" echo "version=${version}" >> "$GITHUB_OUTPUT" - echo "Resolved version: ${version} (tag ${tag})" + echo "Resolved version: ${version}" - name: Substitute version into HTML run: | - set -euo pipefail version='${{ steps.version.outputs.version }}' - # In-place replace every {{VERSION}} occurrence under docs/. find docs -type f \( -name '*.html' -o -name '*.md' -o -name '*.css' -o -name '*.js' \) \ -exec sed -i "s/{{VERSION}}/${version}/g" {} + - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + - name: Deploy to gh-pages branch + uses: JamesIves/github-pages-deploy-action@v4 with: - path: docs - - deploy: - name: Deploy to GitHub Pages - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy - id: deployment - uses: actions/deploy-pages@v4 + branch: gh-pages + folder: docs + clean: true + commit-message: "Deploy docs for version ${{ steps.version.outputs.version }}" \ No newline at end of file From b3ec8d89cf8e26fbdd70b1a452a41c2da9519a58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:15:05 +0000 Subject: [PATCH 04/30] Bump the github-actions group across 1 directory with 3 updates Bumps the github-actions group with 3 updates in the / directory: [actions/setup-node](https://github.com/actions/setup-node), [mymindstorm/setup-emsdk](https://github.com/mymindstorm/setup-emsdk) and [jidicula/clang-format-action](https://github.com/jidicula/clang-format-action). Updates `actions/setup-node` from 6.3.0 to 6.4.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e) Updates `mymindstorm/setup-emsdk` from 14 to 16 - [Release notes](https://github.com/mymindstorm/setup-emsdk/releases) - [Commits](https://github.com/mymindstorm/setup-emsdk/compare/6ab9eb1bda2574c4ddb79809fc9247783eaf9021...4528d102f7230f0e7b276855c01ea1159be0e984) Updates `jidicula/clang-format-action` from 4.17.0 to 4.18.0 - [Release notes](https://github.com/jidicula/clang-format-action/releases) - [Commits](https://github.com/jidicula/clang-format-action/compare/3a18028048f01a66653b4e3bf8d968d2e7e2cb8b...654a770daa28443dd111d133e4083e21c1075674) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 6.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: jidicula/clang-format-action dependency-version: 4.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: mymindstorm/setup-emsdk dependency-version: '16' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/emscripten.yml | 4 ++-- .github/workflows/lint_and_format_check.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/emscripten.yml b/.github/workflows/emscripten.yml index d991a2c..04d2cce 100644 --- a/.github/workflows/emscripten.yml +++ b/.github/workflows/emscripten.yml @@ -5,8 +5,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.2.2 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - - uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: mymindstorm/setup-emsdk@4528d102f7230f0e7b276855c01ea1159be0e984 # v16 - name: Verify run: emcc -v - name: Checkout diff --git a/.github/workflows/lint_and_format_check.yml b/.github/workflows/lint_and_format_check.yml index df040ce..05913e9 100644 --- a/.github/workflows/lint_and_format_check.yml +++ b/.github/workflows/lint_and_format_check.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4.1.7 - name: Run clang-format - uses: jidicula/clang-format-action@3a18028048f01a66653b4e3bf8d968d2e7e2cb8b # v4.17.0 + uses: jidicula/clang-format-action@654a770daa28443dd111d133e4083e21c1075674 # v4.18.0 with: clang-format-version: '17' From b64d014e2f3f83fc8a930a9c420243b2e85bec57 Mon Sep 17 00:00:00 2001 From: fcostaoliveira Date: Mon, 1 Jun 2026 00:48:45 +0100 Subject: [PATCH 05/30] Unroll the integer-part digit scan (straight-line for the common 1-5 digit case) parse_number_string scans the integer part one byte at a time in a while loop, while the fraction already uses the 8-digit SWAR loop. Most integer parts are 1-5 digits, so the loop back-edge dominates. Peel the first five iterations into nested ifs, falling through to the original while for longer runs. Semantics are identical (i = 10*i + digit, advancing p); no behavior change. AWS m8g.metal-24xl (Graviton4), -O3 -march=native, simple_fastfloat_benchmark, from_chars->double. base vs patch measured back-to-back, mean of 2 runs: canada: gcc +3.1%, clang +2.8% mesh: gcc +5.4%, clang +5.1% random: ~flat (1-digit integer part) No regression; gcc and clang agree. Alternatives benchmarked and rejected: reusing loop_parse_if_eight_digits for the integer part regressed 5-8% (integer parts are too short for 8-digit SWAR setup); a counted for(k<5) loop matched on gcc but clang optimized it worse (canada -0.9%). The explicit peel is the only form solidly positive on both compilers. --- include/fast_float/ascii_number.h | 35 +++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index 12c2fdd..22816ec 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -354,13 +354,36 @@ parse_number_string(UC const *p, UC const *pend, uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad) - while ((p != pend) && is_integer(*p)) { - // a multiplication by 10 is cheaper than an arbitrary integer - // multiplication - i = 10 * i + - uint64_t(*p - - UC('0')); // might overflow, we will handle the overflow later + // Straight-line unroll of the integer-part scan: most integer parts are + // 1-5 digits, so peeling the first iterations eliminates the loop back-edge + // for the common case. Semantics are identical to the original `while` loop: + // i = 10*i + digit, advancing p. + if ((p != pend) && is_integer(*p)) { + i = uint64_t(*p - UC('0')); ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + while ((p != pend) && is_integer(*p)) { + // a multiplication by 10 is cheaper than an arbitrary integer + // multiplication + i = 10 * i + + uint64_t(*p - UC('0')); // might overflow, handled later + ++p; + } + } + } + } + } } UC const *const end_of_integer_part = p; int64_t digit_count = int64_t(end_of_integer_part - start_digits); From 7589a4fea569d5bebd2a474136e8ec9e00ce0335 Mon Sep 17 00:00:00 2001 From: fcostaoliveira Date: Mon, 1 Jun 2026 10:55:04 +0100 Subject: [PATCH 06/30] Add a 4-digit SWAR follow-up to loop_parse_if_eight_digits (clang) After the 8-digit SWAR block loop, consume a remaining 4-7 digit run in one read4_to_u32 + parse_four_digits_unrolled step instead of byte-by-byte (reusing the existing 4-digit helpers). The parsed result is identical; this is purely a faster way to consume the same digits. Gated to clang: on gcc the extra 4-digit check regresses inputs whose remainder is < 4 digits (e.g. the 17-digit fraction of uniform [0,1] -> -3% on 'random'), because the check becomes pure overhead there; clang does not show that. m8g.metal-24xl (Graviton4), -O3 -march=native, simple_fastfloat_benchmark, from_chars->double, clang 18, base vs patch back-to-back (2 samples): canada.txt +11.7%, mesh.txt +7.4%, random ~flat. No regression. --- include/fast_float/ascii_number.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index 12c2fdd..1c9c857 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -266,6 +266,21 @@ loop_parse_if_eight_digits(char const *&p, char const *const pend, p)); // in rare cases, this will overflow, but that's ok p += 8; } + // Consume a remaining 4-7 digit run in a single SWAR step instead of + // byte-by-byte (reuses the existing 4-digit helpers). The parsed result is + // identical either way. Gated to clang: on gcc the extra 4-digit check + // regresses inputs whose remainder is shorter than 4 digits (it becomes pure + // overhead there); clang does not show that. +#if defined(__clang__) + if ((pend - p) >= 4) { + uint32_t const val4 = read4_to_u32(p); + if (is_made_of_four_digits_fast(val4)) { + i = i * 10000 + + parse_four_digits_unrolled(val4); // may overflow, that's ok + p += 4; + } + } +#endif } enum class parse_error { From b20c4209648f98950812d4eaf35d9daf672bc63e Mon Sep 17 00:00:00 2001 From: fcostaoliveira Date: Mon, 1 Jun 2026 13:01:10 +0100 Subject: [PATCH 07/30] tests: parallelize the exhaustive midpoint sweep across hardware threads --- tests/CMakeLists.txt | 5 + tests/exhaustive32_midpoint.cpp | 174 ++++++++++++++++++-------------- 2 files changed, 104 insertions(+), 75 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a053581..19f2452 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,10 @@ cmake_minimum_required(VERSION 3.11 FATAL_ERROR) include(FetchContent) +# Some tests (the exhaustive sweeps) parallelize across std::thread. +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + option(SYSTEM_DOCTEST "Use system copy of doctest" OFF) option(FASTFLOAT_SUPPLEMENTAL_TESTS "Run supplemental tests" ON) @@ -49,6 +53,7 @@ function(fast_float_add_cpp_test TEST_NAME) target_compile_options(${TEST_NAME} PUBLIC -Wsign-compare -Wshadow -Wwrite-strings -Wpointer-arith -Winit-self -Wconversion -Wsign-conversion) endif() target_link_libraries(${TEST_NAME} PUBLIC fast_float supplemental-data) + target_link_libraries(${TEST_NAME} PUBLIC Threads::Threads) if (NOT SYSTEM_DOCTEST) target_link_libraries(${TEST_NAME} PUBLIC doctest) else () diff --git a/tests/exhaustive32_midpoint.cpp b/tests/exhaustive32_midpoint.cpp index 0b97e2e..d1fc160 100644 --- a/tests/exhaustive32_midpoint.cpp +++ b/tests/exhaustive32_midpoint.cpp @@ -1,5 +1,6 @@ #include "fast_float/fast_float.h" +#include #include #include #include @@ -7,6 +8,8 @@ #include #include #include +#include +#include #if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) // Anything at all that is related to cygwin, msys and so forth will @@ -74,86 +77,107 @@ void strtof_from_string(char const *st, float &d) { } } -bool allvalues() { +// Checks a single 32-bit word (interpreted as a float). Returns true if the +// parser agrees with the reference, false (after logging) on a mismatch. +bool check_word(uint32_t word) { char buffer[64]; - for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) { - float v; - if ((w % 1048576) == 0) { - std::cout << "."; - std::cout.flush(); - } - uint32_t word = uint32_t(w); - memcpy(&v, &word, sizeof(v)); - if (std::isfinite(v)) { - float nextf = std::nextafterf(v, INFINITY); - if (copysign(1, v) != copysign(1, nextf)) { - continue; - } - if (!std::isfinite(nextf)) { - continue; - } - double v1{v}; - assert(float(v1) == v); - double v2{nextf}; - assert(float(v2) == nextf); - double midv{v1 + (v2 - v1) / 2}; - float expected_midv = float(midv); - - char const *string_end = to_string(midv, buffer); - float str_answer; - strtof_from_string(buffer, str_answer); - - float result_value; - auto result = fast_float::from_chars(buffer, string_end, result_value); - // Starting with version 4.0 for fast_float, we return result_out_of_range - // if the value is either too small (too close to zero) or too large - // (effectively infinity). So std::errc::result_out_of_range is normal for - // well-formed input strings. - if (result.ec != std::errc() && - result.ec != std::errc::result_out_of_range) { - std::cerr << "parsing error ? " << buffer << std::endl; - return false; - } - if (std::isnan(v)) { - if (!std::isnan(result_value)) { - std::cerr << "not nan" << buffer << std::endl; - std::cerr << "v " << std::hexfloat << v << std::endl; - std::cerr << "v2 " << std::hexfloat << v2 << std::endl; - std::cerr << "midv " << std::hexfloat << midv << std::endl; - std::cerr << "expected_midv " << std::hexfloat << expected_midv - << std::endl; - return false; - } - } else if (copysign(1, result_value) != copysign(1, v)) { - std::cerr << buffer << std::endl; - std::cerr << "v " << std::hexfloat << v << std::endl; - std::cerr << "v2 " << std::hexfloat << v2 << std::endl; - std::cerr << "midv " << std::hexfloat << midv << std::endl; - std::cerr << "expected_midv " << std::hexfloat << expected_midv - << std::endl; - std::cerr << "I got " << std::hexfloat << result_value - << " but I was expecting " << v << std::endl; - return false; - } else if (result_value != str_answer) { - std::cerr << "no match ? " << buffer << std::endl; - std::cerr << "v " << std::hexfloat << v << std::endl; - std::cerr << "v2 " << std::hexfloat << v2 << std::endl; - std::cerr << "midv " << std::hexfloat << midv << std::endl; - std::cerr << "expected_midv " << std::hexfloat << expected_midv - << std::endl; - std::cout << "started with " << std::hexfloat << midv << std::endl; - std::cout << "round down to " << std::hexfloat << str_answer - << std::endl; - std::cout << "got back " << std::hexfloat << result_value << std::endl; - std::cout << std::dec; - return false; - } - } + float v; + memcpy(&v, &word, sizeof(v)); + if (!std::isfinite(v)) { + return true; + } + float nextf = std::nextafterf(v, INFINITY); + if (copysign(1, v) != copysign(1, nextf)) { + return true; + } + if (!std::isfinite(nextf)) { + return true; + } + double v1{v}; + assert(float(v1) == v); + double v2{nextf}; + assert(float(v2) == nextf); + double midv{v1 + (v2 - v1) / 2}; + float expected_midv = float(midv); + + char const *string_end = to_string(midv, buffer); + float str_answer; + strtof_from_string(buffer, str_answer); + + float result_value; + auto result = fast_float::from_chars(buffer, string_end, result_value); + // Starting with version 4.0 for fast_float, we return result_out_of_range + // if the value is either too small (too close to zero) or too large + // (effectively infinity). So std::errc::result_out_of_range is normal for + // well-formed input strings. + if (result.ec != std::errc() && result.ec != std::errc::result_out_of_range) { + std::cerr << "parsing error ? " << buffer << std::endl; + return false; + } + if (std::isnan(v)) { + if (!std::isnan(result_value)) { + std::cerr << "not nan" << buffer << std::endl; + std::cerr << "v " << std::hexfloat << v << std::endl; + std::cerr << "v2 " << std::hexfloat << v2 << std::endl; + std::cerr << "midv " << std::hexfloat << midv << std::endl; + std::cerr << "expected_midv " << std::hexfloat << expected_midv + << std::endl; + return false; + } + } else if (copysign(1, result_value) != copysign(1, v)) { + std::cerr << buffer << std::endl; + std::cerr << "v " << std::hexfloat << v << std::endl; + std::cerr << "v2 " << std::hexfloat << v2 << std::endl; + std::cerr << "midv " << std::hexfloat << midv << std::endl; + std::cerr << "expected_midv " << std::hexfloat << expected_midv + << std::endl; + std::cerr << "I got " << std::hexfloat << result_value + << " but I was expecting " << v << std::endl; + return false; + } else if (result_value != str_answer) { + std::cerr << "no match ? " << buffer << std::endl; + std::cerr << "v " << std::hexfloat << v << std::endl; + std::cerr << "v2 " << std::hexfloat << v2 << std::endl; + std::cerr << "midv " << std::hexfloat << midv << std::endl; + std::cerr << "expected_midv " << std::hexfloat << expected_midv + << std::endl; + std::cout << "started with " << std::hexfloat << midv << std::endl; + std::cout << "round down to " << std::hexfloat << str_answer << std::endl; + std::cout << "got back " << std::hexfloat << result_value << std::endl; + std::cout << std::dec; + return false; } - std::cout << std::endl; return true; } +// Sweeps the whole 2^32 float space, split across hardware threads (the values +// are independent). Returns false as soon as any word mismatches. +bool allvalues() { + unsigned int nthreads = std::thread::hardware_concurrency(); + if (nthreads == 0) { + nthreads = 1; + } + std::atomic ok{true}; + std::vector workers; + workers.reserve(nthreads); + for (unsigned int t = 0; t < nthreads; t++) { + workers.emplace_back([t, nthreads, &ok]() { + for (uint64_t w = t; + w <= 0xFFFFFFFF && ok.load(std::memory_order_relaxed); + w += nthreads) { + if (!check_word(uint32_t(w))) { + ok.store(false, std::memory_order_relaxed); + return; + } + } + }); + } + for (std::thread &worker : workers) { + worker.join(); + } + return ok.load(); +} + inline void Assert(bool Assertion) { #if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) || \ defined(sun) || defined(__sun) From b642d9202f88d151e7ff63abd124f802bc652253 Mon Sep 17 00:00:00 2001 From: fcostaoliveira Date: Mon, 1 Jun 2026 21:09:46 +0100 Subject: [PATCH 08/30] tests: parallelize exhaustive32 and exhaustive32_64 sweeps too Same std::thread split as exhaustive32_midpoint; preserves each test's existing failure behavior (abort for exhaustive32, stop-flag for exhaustive32_64). --- tests/exhaustive32.cpp | 90 ++++++++++++++++++++++----------------- tests/exhaustive32_64.cpp | 48 ++++++++++++++------- 2 files changed, 84 insertions(+), 54 deletions(-) diff --git a/tests/exhaustive32.cpp b/tests/exhaustive32.cpp index 5e7de2d..455d8af 100644 --- a/tests/exhaustive32.cpp +++ b/tests/exhaustive32.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include template char *to_string(T d, char *buffer) { auto written = std::snprintf(buffer, 64, "%.*e", @@ -15,47 +17,59 @@ template char *to_string(T d, char *buffer) { return buffer + written; } -void allvalues() { +// Checks a single 32-bit word (interpreted as a float); aborts on a mismatch. +void check_word(uint32_t word) { char buffer[64]; - for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) { - float v; - if ((w % 1048576) == 0) { - std::cout << "."; - std::cout.flush(); - } - uint32_t word = uint32_t(w); - memcpy(&v, &word, sizeof(v)); + float v; + memcpy(&v, &word, sizeof(v)); - { - char const *string_end = to_string(v, buffer); - float result_value; - auto result = fast_float::from_chars(buffer, string_end, result_value); - // Starting with version 4.0 for fast_float, we return result_out_of_range - // if the value is either too small (too close to zero) or too large - // (effectively infinity). So std::errc::result_out_of_range is normal for - // well-formed input strings. - if (result.ec != std::errc() && - result.ec != std::errc::result_out_of_range) { - std::cerr << "parsing error ? " << buffer << std::endl; - abort(); - } - if (std::isnan(v)) { - if (!std::isnan(result_value)) { - std::cerr << "not nan" << buffer << std::endl; - abort(); - } - } else if (copysign(1, result_value) != copysign(1, v)) { - std::cerr << "I got " << std::hexfloat << result_value - << " but I was expecting " << v << std::endl; - abort(); - } else if (result_value != v) { - std::cerr << "no match ? " << buffer << std::endl; - std::cout << "started with " << std::hexfloat << v << std::endl; - std::cout << "got back " << std::hexfloat << result_value << std::endl; - std::cout << std::dec; - abort(); - } + char const *string_end = to_string(v, buffer); + float result_value; + auto result = fast_float::from_chars(buffer, string_end, result_value); + // Starting with version 4.0 for fast_float, we return result_out_of_range + // if the value is either too small (too close to zero) or too large + // (effectively infinity). So std::errc::result_out_of_range is normal for + // well-formed input strings. + if (result.ec != std::errc() && result.ec != std::errc::result_out_of_range) { + std::cerr << "parsing error ? " << buffer << std::endl; + abort(); + } + if (std::isnan(v)) { + if (!std::isnan(result_value)) { + std::cerr << "not nan" << buffer << std::endl; + abort(); } + } else if (copysign(1, result_value) != copysign(1, v)) { + std::cerr << "I got " << std::hexfloat << result_value + << " but I was expecting " << v << std::endl; + abort(); + } else if (result_value != v) { + std::cerr << "no match ? " << buffer << std::endl; + std::cout << "started with " << std::hexfloat << v << std::endl; + std::cout << "got back " << std::hexfloat << result_value << std::endl; + std::cout << std::dec; + abort(); + } +} + +// Sweeps the whole 2^32 float space, split across hardware threads (the values +// are independent); check_word() aborts on the first mismatch. +void allvalues() { + unsigned int nthreads = std::thread::hardware_concurrency(); + if (nthreads == 0) { + nthreads = 1; + } + std::vector workers; + workers.reserve(nthreads); + for (unsigned int t = 0; t < nthreads; t++) { + workers.emplace_back([t, nthreads]() { + for (uint64_t w = t; w <= 0xFFFFFFFF; w += nthreads) { + check_word(uint32_t(w)); + } + }); + } + for (std::thread &worker : workers) { + worker.join(); } std::cout << std::endl; } diff --git a/tests/exhaustive32_64.cpp b/tests/exhaustive32_64.cpp index c453f28..b520352 100644 --- a/tests/exhaustive32_64.cpp +++ b/tests/exhaustive32_64.cpp @@ -1,6 +1,7 @@ #include "fast_float/fast_float.h" +#include #include #include #include @@ -9,6 +10,8 @@ #include #include #include +#include +#include template char *to_string(T d, char *buffer) { auto written = std::snprintf(buffer, 64, "%.*e", @@ -45,25 +48,38 @@ bool basic_test_64bit(std::string vals, double val) { return true; } +// Sweeps the whole 2^32 float space (widened to double), split across hardware +// threads (the values are independent); stops at the first mismatch. void all_32bit_values() { - char buffer[64]; - for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) { - float v32; - if ((w % 1048576) == 0) { - std::cout << "."; - std::cout.flush(); - } - uint32_t word = uint32_t(w); - memcpy(&v32, &word, sizeof(v32)); - double v = v32; + unsigned int nthreads = std::thread::hardware_concurrency(); + if (nthreads == 0) { + nthreads = 1; + } + std::atomic ok{true}; + std::vector workers; + workers.reserve(nthreads); + for (unsigned int t = 0; t < nthreads; t++) { + workers.emplace_back([t, nthreads, &ok]() { + char buffer[64]; + for (uint64_t w = t; + w <= 0xFFFFFFFF && ok.load(std::memory_order_relaxed); + w += nthreads) { + float v32; + uint32_t word = uint32_t(w); + memcpy(&v32, &word, sizeof(v32)); + double v = v32; - { - char const *string_end = to_string(v, buffer); - std::string s(buffer, size_t(string_end - buffer)); - if (!basic_test_64bit(s, v)) { - return; + char const *string_end = to_string(v, buffer); + std::string s(buffer, size_t(string_end - buffer)); + if (!basic_test_64bit(s, v)) { + ok.store(false, std::memory_order_relaxed); + return; + } } - } + }); + } + for (std::thread &worker : workers) { + worker.join(); } std::cout << std::endl; } From cfd12ebcf1f82c4fd44a950b1815dd0549bc8d89 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 1 Jun 2026 18:07:41 -0400 Subject: [PATCH 09/30] 8.2.6 --- CMakeLists.txt | 2 +- README.md | 6 +++--- include/fast_float/float_common.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e7cedc2..f4ebafa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.2.5 LANGUAGES CXX) +project(fast_float VERSION 8.2.6 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/README.md b/README.md index 5e44349..193a977 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,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.2.5 + GIT_TAG tags/v8.2.6 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -547,7 +547,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.2.5) + GIT_TAG v8.2.6) ``` ## Using as single header @@ -559,7 +559,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 403eea1..3e91c57 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 2 -#define FASTFLOAT_VERSION_PATCH 5 +#define FASTFLOAT_VERSION_PATCH 6 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) From f0ed8cdf52c179e1068b953ad74dfbde1f2de8cb Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 1 Jun 2026 18:28:09 -0400 Subject: [PATCH 10/30] display the latest version. --- docs/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index fd2d17d..f9785cf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -221,7 +221,7 @@ constexpr double pi = parse("3.1415"); // computed at compile time
FetchContent_Declare(
   fast_float
   GIT_REPOSITORY https://github.com/fastfloat/fast_float.git
-  GIT_TAG tags/v{{VERSION}}
+  GIT_TAG tags/v{{VERSION}}
   GIT_SHALLOW TRUE)
 FetchContent_MakeAvailable(fast_float)
 target_link_libraries(myprogram PUBLIC fast_float)
@@ -232,13 +232,13 @@ target_link_libraries(myprogram PUBLIC fast_float)
CPMAddPackage(
   NAME fast_float
   GITHUB_REPOSITORY "fastfloat/fast_float"
-  GIT_TAG v{{VERSION}})
+ GIT_TAG v{{VERSION}})

Single header

Download a pre-amalgamated header — no build system required.

-
curl -LO https://github.com/fastfloat/fast_float/releases/download/v{{VERSION}}/fast_float.h
+
curl -LO https://github.com/fastfloat/fast_float/releases/download/v{{VERSION}}/fast_float.h
From 1b11407da9d5b987a37c09dfa75ebcc90cf6f7aa Mon Sep 17 00:00:00 2001 From: Jonathan Wakely Date: Tue, 2 Jun 2026 15:10:14 +0100 Subject: [PATCH 11/30] Fix spelling Run clang-format to reformat the long lines. --- include/fast_float/fast_float.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/fast_float/fast_float.h b/include/fast_float/fast_float.h index eb822f5..1e79ac6 100644 --- a/include/fast_float/fast_float.h +++ b/include/fast_float/fast_float.h @@ -7,12 +7,12 @@ namespace fast_float { /** * This function parses the character sequence [first,last) for a number. It - * parses floating-point numbers expecting a locale-indepent format equivalent - * to what is used by std::strtod in the default ("C") locale. The resulting - * floating-point value is the closest floating-point values (using either float - * or double), using the "round to even" convention for values that would - * otherwise fall right in-between two values. That is, we provide exact parsing - * according to the IEEE standard. + * parses floating-point numbers expecting a locale-independent format + * equivalent to what is used by std::strtod in the default ("C") locale. The + * resulting floating-point value is the closest floating-point values (using + * either float or double), using the "round to even" convention for values that + * would otherwise fall right in-between two values. That is, we provide exact + * parsing according to the IEEE standard. * * Given a successful parse, the pointer (`ptr`) in the returned value is set to * point right after the parsed number, and the `value` referenced is set to the From cb5d9cd9a4d46a114e89181d925a6940c784b31d Mon Sep 17 00:00:00 2001 From: fcostaoliveira Date: Wed, 3 Jun 2026 09:30:42 +0100 Subject: [PATCH 12/30] Skip materializing the integer/fraction spans on the hot path parsed_number_string_t carries two span members (integer, fraction) that are only read on the rare slow paths (digit_comp, and the >19-significant- digit truncation recompute). Materializing them on every parse forces the ~56/64- byte struct to be written out and marshaled through the by-value return, which shows up as backend/store pressure on the hot path. This adds a runtime `store_spans` flag (default true, so all existing callers are unchanged) to parse_number_string; from_chars_float_advanced parses with it false, attempts the Clinger and Eisel-Lemire fast paths inline, and only re-parses with spans on the two rare slow branches. The re-parse is pushed into a single `fastfloat_noinline` (noinline+cold) helper so the force-inlined hot scanner is emitted once rather than duplicated into the caller (without this the extra inline copies regress some targets, e.g. ARM gcc, by bloating the hot frame and lengthening the loop-carried dependency chain). A runtime flag is used deliberately rather than a template parameter: a template would create a second instantiation of the whole scanner whose icache cost wipes out the gain. Measured (per-parser microbench, median of 5, pinned core), fast_float from_chars /, vs the current tip: - Intel Ice Lake (Xeon 8360Y): +17-19% (gcc), Intel TMA shows backend-bound 26.0% -> 2.2% and retiring 60.3% -> 77.3% on short floats (the eliminated span spill), with -36% pipeline slots. - Intel Cascade Lake (Xeon 6248): +18-22% (gcc), +13-23% (clang). - ARM Neoverse-V2 (Graviton4): +73-196% (gcc), +8-11% (clang) -- the struct spill dominated the gcc hot loop there. Correctness: the full float exhaustive suite (exhaustive32, exhaustive32_64, exhaustive32_midpoint, random64) passes, and a 2^32 sweep is byte-identical to the current tip. Public from_chars / from_chars_advanced / parsed_number_string_t are unchanged. --- include/fast_float/ascii_number.h | 62 ++++++++++++++++++++----------- include/fast_float/float_common.h | 9 +++++ include/fast_float/parse_number.h | 60 +++++++++++++++++++++++++++--- 3 files changed, 104 insertions(+), 27 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index 64c3d3f..b45e47e 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -330,10 +330,18 @@ report_parse_error(UC const *p, parse_error error) { // Assuming that you use no more than 19 digits, this will // parse an ASCII string. +// +// store_spans is a *runtime* flag (not a template parameter, deliberately: a +// template would create a second instantiation of this whole function and the +// extra icache pressure wipes out the gain). When false, the integer/fraction +// spans (read only by the rare digit_comp slow path) are not materialized, which +// keeps the fat parsed_number_string_t off the hot path. The caller re-parses +// with store_spans=true if the slow path is actually reached. template fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t parse_number_string(UC const *p, UC const *pend, - parse_options_t options) noexcept { + parse_options_t options, + bool store_spans = true) noexcept { chars_format const fmt = detail::adjust_for_feature_macros(options.format); UC const decimal_point = options.decimal_point; @@ -402,7 +410,9 @@ parse_number_string(UC const *p, UC const *pend, } UC const *const end_of_integer_part = p; int64_t digit_count = int64_t(end_of_integer_part - start_digits); - answer.integer = span(start_digits, size_t(digit_count)); + if (store_spans) { + answer.integer = span(start_digits, size_t(digit_count)); + } FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) { // at least 1 digit in integer part, without leading zeros if (digit_count == 0) { @@ -429,7 +439,9 @@ parse_number_string(UC const *p, UC const *pend, i = i * 10 + digit; // in rare cases, this will overflow, but that's ok } exponent = before - p; - answer.fraction = span(before, size_t(p - before)); + if (store_spans) { + answer.fraction = span(before, size_t(p - before)); + } digit_count -= exponent; } FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) { @@ -514,29 +526,35 @@ parse_number_string(UC const *p, UC const *pend, if (digit_count > 19) { answer.too_many_digits = true; - // Let us start again, this time, avoiding overflows. - // We don't need to call if is_integer, since we use the - // pre-tokenized spans from above. - i = 0; - p = answer.integer.ptr; - UC const *int_end = p + answer.integer.len(); - uint64_t const minimal_nineteen_digit_integer{1000000000000000000}; - while ((i < minimal_nineteen_digit_integer) && (p != int_end)) { - i = i * 10 + uint64_t(*p - UC('0')); - ++p; - } - if (i >= minimal_nineteen_digit_integer) { // We have a big integer - exponent = end_of_integer_part - p + exp_number; - } else { // We have a value with a fractional component. - p = answer.fraction.ptr; - UC const *frac_end = p + answer.fraction.len(); - while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) { + // The truncation recompute below reads the integer/fraction spans. When + // store_spans is false we didn't materialize them, so just flag + // too_many_digits; the caller re-parses with store_spans=true to obtain + // the corrected mantissa/exponent before taking the slow path. + if (store_spans) { + // Let us start again, this time, avoiding overflows. + // We don't need to call if is_integer, since we use the + // pre-tokenized spans from above. + i = 0; + p = answer.integer.ptr; + UC const *int_end = p + answer.integer.len(); + uint64_t const minimal_nineteen_digit_integer{1000000000000000000}; + while ((i < minimal_nineteen_digit_integer) && (p != int_end)) { i = i * 10 + uint64_t(*p - UC('0')); ++p; } - exponent = answer.fraction.ptr - p + exp_number; + if (i >= minimal_nineteen_digit_integer) { // We have a big integer + exponent = end_of_integer_part - p + exp_number; + } else { // We have a value with a fractional component. + p = answer.fraction.ptr; + UC const *frac_end = p + answer.fraction.len(); + while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) { + i = i * 10 + uint64_t(*p - UC('0')); + ++p; + } + exponent = answer.fraction.ptr - p + exp_number; + } + // We have now corrected both exponent and i, to a truncated value } - // We have now corrected both exponent and i, to a truncated value } } answer.exponent = exponent; diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 3e91c57..bd41bf1 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -197,6 +197,15 @@ using parse_options = parse_options_t; #define fastfloat_really_inline inline __attribute__((always_inline)) #endif +// Force a function OUT of line and onto the cold path. Used for the rare +// slow-path re-parse so the force-inlined hot scanner is not duplicated into +// the caller (which bloated the hot frame and hurt ILP on some targets). +#ifdef FASTFLOAT_VISUAL_STUDIO +#define fastfloat_noinline __declspec(noinline) +#else +#define fastfloat_noinline __attribute__((noinline, cold)) +#endif + #ifndef FASTFLOAT_ASSERT #define FASTFLOAT_ASSERT(x) \ { ((void)(x)); } diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index ff9c53d..d996002 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -289,6 +289,23 @@ from_chars_advanced(parsed_number_string_t &pns, T &value) noexcept { return answer; } +// Cold, out-of-line slow path: re-parse materializing the integer/fraction +// spans the hot no-span parse skipped, then run the full algorithm. Marked +// noinline+cold so the force-inlined spans scanner is emitted ONCE off the hot +// path rather than duplicated into from_chars_float_advanced (which bloated the +// hot frame). from_chars_advanced already handles both the too_many_digits +// disambiguation and the am.power2<0 digit_comp recompute, so both slow branches +// collapse to one helper call. +template +fastfloat_noinline FASTFLOAT_CONSTEXPR20 from_chars_result_t +parse_number_slow_path(UC const *first, UC const *last, T &value, + parse_options_t options, bool bjf) noexcept { + parsed_number_string_t pns = + bjf ? parse_number_string(first, last, options, true) + : parse_number_string(first, last, options, true); + return from_chars_advanced(pns, value); +} + template fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t from_chars_float_advanced(UC const *first, UC const *last, T &value, @@ -312,10 +329,15 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, answer.ptr = first; return answer; } + bool const bjf = uint64_t(fmt & detail::basic_json_fmt) != 0; + + // Fast path: parse WITHOUT materializing the integer/fraction spans (read only + // by the rare slow paths). Skipping their stores keeps the fat + // parsed_number_string_t off the hot path. store_spans is a runtime argument, + // so this reuses the single parse_number_string instantiation. parsed_number_string_t pns = - uint64_t(fmt & detail::basic_json_fmt) - ? parse_number_string(first, last, options) - : parse_number_string(first, last, options); + bjf ? parse_number_string(first, last, options, false) + : parse_number_string(first, last, options, false); if (!pns.valid) { if (uint64_t(fmt & chars_format::no_infnan)) { answer.ec = std::errc::invalid_argument; @@ -326,8 +348,36 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, } } - // call overload that takes parsed_number_string_t directly. - return from_chars_advanced(pns, value); + // Slow path A (rare): > 19 significant digits. The no-span parse left the + // mantissa un-truncated and skipped the span-based recompute; the cold helper + // re-parses with spans and runs the full algorithm. + if (pns.too_many_digits) { + return parse_number_slow_path(first, last, value, options, bjf); + } + + answer.ec = std::errc(); // be optimistic + answer.ptr = pns.lastmatch; + + if (clinger_fast_path_impl(pns.mantissa, pns.exponent, pns.negative, value)) { + return answer; + } + + adjusted_mantissa am = + compute_float>(pns.exponent, pns.mantissa); + // Slow path B (rare): Eisel-Lemire could not resolve; digit_comp needs the + // integer/fraction spans. Route to the cold helper (clinger there is a + // dead-effect since it already failed here; the cold re-parse + digit_comp via + // from_chars_advanced reproduces this branch). + if (am.power2 < 0) { + return parse_number_slow_path(first, last, value, options, bjf); + } + to_float(pns.negative, am, value); + // Test for over/underflow. + if ((pns.mantissa != 0 && am.mantissa == 0 && am.power2 == 0) || + am.power2 == binary_format::infinite_power()) { + answer.ec = std::errc::result_out_of_range; + } + return answer; } template From 3067491f410334883f3c82ccc9eda957714bc7a8 Mon Sep 17 00:00:00 2001 From: fcostaoliveira Date: Wed, 3 Jun 2026 09:35:26 +0100 Subject: [PATCH 13/30] clang-format (clang-format-17 comment reflow + signature wrap; no semantic change) --- include/fast_float/ascii_number.h | 9 ++++----- include/fast_float/parse_number.h | 12 ++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index b45e47e..e431cbc 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -334,13 +334,12 @@ report_parse_error(UC const *p, parse_error error) { // store_spans is a *runtime* flag (not a template parameter, deliberately: a // template would create a second instantiation of this whole function and the // extra icache pressure wipes out the gain). When false, the integer/fraction -// spans (read only by the rare digit_comp slow path) are not materialized, which -// keeps the fat parsed_number_string_t off the hot path. The caller re-parses -// with store_spans=true if the slow path is actually reached. +// spans (read only by the rare digit_comp slow path) are not materialized, +// which keeps the fat parsed_number_string_t off the hot path. The caller +// re-parses with store_spans=true if the slow path is actually reached. template fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t -parse_number_string(UC const *p, UC const *pend, - parse_options_t options, +parse_number_string(UC const *p, UC const *pend, parse_options_t options, bool store_spans = true) noexcept { chars_format const fmt = detail::adjust_for_feature_macros(options.format); UC const decimal_point = options.decimal_point; diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index d996002..be38781 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -294,8 +294,8 @@ from_chars_advanced(parsed_number_string_t &pns, T &value) noexcept { // noinline+cold so the force-inlined spans scanner is emitted ONCE off the hot // path rather than duplicated into from_chars_float_advanced (which bloated the // hot frame). from_chars_advanced already handles both the too_many_digits -// disambiguation and the am.power2<0 digit_comp recompute, so both slow branches -// collapse to one helper call. +// disambiguation and the am.power2<0 digit_comp recompute, so both slow +// branches collapse to one helper call. template fastfloat_noinline FASTFLOAT_CONSTEXPR20 from_chars_result_t parse_number_slow_path(UC const *first, UC const *last, T &value, @@ -331,8 +331,8 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, } bool const bjf = uint64_t(fmt & detail::basic_json_fmt) != 0; - // Fast path: parse WITHOUT materializing the integer/fraction spans (read only - // by the rare slow paths). Skipping their stores keeps the fat + // Fast path: parse WITHOUT materializing the integer/fraction spans (read + // only by the rare slow paths). Skipping their stores keeps the fat // parsed_number_string_t off the hot path. store_spans is a runtime argument, // so this reuses the single parse_number_string instantiation. parsed_number_string_t pns = @@ -366,8 +366,8 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, compute_float>(pns.exponent, pns.mantissa); // Slow path B (rare): Eisel-Lemire could not resolve; digit_comp needs the // integer/fraction spans. Route to the cold helper (clinger there is a - // dead-effect since it already failed here; the cold re-parse + digit_comp via - // from_chars_advanced reproduces this branch). + // dead-effect since it already failed here; the cold re-parse + digit_comp + // via from_chars_advanced reproduces this branch). if (am.power2 < 0) { return parse_number_slow_path(first, last, value, options, bjf); } From b72e07132c1a36adf4c4c29665931e322ab704ae Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Fri, 5 Jun 2026 22:01:27 -0400 Subject: [PATCH 14/30] let us using 'unlikely' hints. --- include/fast_float/float_common.h | 24 ++++++++++++++++++------ include/fast_float/parse_number.h | 20 ++++++++++---------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index bd41bf1..ee7a6d0 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -197,13 +197,25 @@ using parse_options = parse_options_t; #define fastfloat_really_inline inline __attribute__((always_inline)) #endif -// Force a function OUT of line and onto the cold path. Used for the rare -// slow-path re-parse so the force-inlined hot scanner is not duplicated into -// the caller (which bloated the hot frame and hurt ILP on some targets). -#ifdef FASTFLOAT_VISUAL_STUDIO -#define fastfloat_noinline __declspec(noinline) +// Branch-probability hint marking the rare slow-path branches as cold, so the +// optimizer keeps the out-of-line slow-path re-parse off the hot path (and does +// not duplicate the force-inlined hot scanner into the caller, which bloated +// the hot frame and hurt ILP on some targets). Used at the call site as +// if fastfloat_unlikely(cond) { ... } +// (the macro supplies the parentheses). It expands to the standard [[unlikely]] +// attribute in C++20 or newer, otherwise to __builtin_expect on GCC/Clang, or +// to a no-op elsewhere (e.g. pre-C++20 MSVC, which has no equivalent hint). +// The [[unlikely]] branch is gated on the language version, not just on +// __has_cpp_attribute: GCC and Clang report the attribute as available even +// under -std=c++17, where using it would trip -Wc++20-extensions/-Werror. +#if (__cplusplus >= 202002L || \ + (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) && \ + defined(__has_cpp_attribute) && __has_cpp_attribute(unlikely) >= 201803L +#define fastfloat_unlikely(x) (x) [[unlikely]] +#elif defined(__GNUC__) || defined(__clang__) +#define fastfloat_unlikely(x) (__builtin_expect(!!(x), 0)) #else -#define fastfloat_noinline __attribute__((noinline, cold)) +#define fastfloat_unlikely(x) (x) #endif #ifndef FASTFLOAT_ASSERT diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index be38781..a844bc8 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -289,15 +289,15 @@ from_chars_advanced(parsed_number_string_t &pns, T &value) noexcept { return answer; } -// Cold, out-of-line slow path: re-parse materializing the integer/fraction -// spans the hot no-span parse skipped, then run the full algorithm. Marked -// noinline+cold so the force-inlined spans scanner is emitted ONCE off the hot -// path rather than duplicated into from_chars_float_advanced (which bloated the -// hot frame). from_chars_advanced already handles both the too_many_digits -// disambiguation and the am.power2<0 digit_comp recompute, so both slow -// branches collapse to one helper call. +// Slow path: re-parse materializing the integer/fraction spans the hot no-span +// parse skipped, then run the full algorithm. The two callers reach it only +// through a fastfloat_unlikely branch, so the optimizer keeps this re-parse off +// the hot path on its own (no function-level noinline needed). +// from_chars_advanced already handles both the too_many_digits disambiguation +// and the am.power2<0 digit_comp recompute, so both slow branches collapse to +// one helper call. template -fastfloat_noinline FASTFLOAT_CONSTEXPR20 from_chars_result_t +FASTFLOAT_CONSTEXPR20 from_chars_result_t parse_number_slow_path(UC const *first, UC const *last, T &value, parse_options_t options, bool bjf) noexcept { parsed_number_string_t pns = @@ -351,7 +351,7 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, // Slow path A (rare): > 19 significant digits. The no-span parse left the // mantissa un-truncated and skipped the span-based recompute; the cold helper // re-parses with spans and runs the full algorithm. - if (pns.too_many_digits) { + if fastfloat_unlikely (pns.too_many_digits) { return parse_number_slow_path(first, last, value, options, bjf); } @@ -368,7 +368,7 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, // integer/fraction spans. Route to the cold helper (clinger there is a // dead-effect since it already failed here; the cold re-parse + digit_comp // via from_chars_advanced reproduces this branch). - if (am.power2 < 0) { + if fastfloat_unlikely (am.power2 < 0) { return parse_number_slow_path(first, last, value, options, bjf); } to_float(pns.negative, am, value); From 520fded4a398152c854100556614086ad196e35f Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sat, 6 Jun 2026 13:13:49 -0400 Subject: [PATCH 15/30] adressing comments by @jwakely --- include/fast_float/float_common.h | 26 +++++++++++++++++++------- include/fast_float/parse_number.h | 7 +++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index ee7a6d0..479febc 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -203,15 +203,27 @@ using parse_options = parse_options_t; // the hot frame and hurt ILP on some targets). Used at the call site as // if fastfloat_unlikely(cond) { ... } // (the macro supplies the parentheses). It expands to the standard [[unlikely]] -// attribute in C++20 or newer, otherwise to __builtin_expect on GCC/Clang, or +// attribute when supported, otherwise to __builtin_expect on GCC/Clang, or // to a no-op elsewhere (e.g. pre-C++20 MSVC, which has no equivalent hint). -// The [[unlikely]] branch is gated on the language version, not just on -// __has_cpp_attribute: GCC and Clang report the attribute as available even -// under -std=c++17, where using it would trip -Wc++20-extensions/-Werror. -#if (__cplusplus >= 202002L || \ - (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) && \ - defined(__has_cpp_attribute) && __has_cpp_attribute(unlikely) >= 201803L +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(unlikely) >= 201803L +#define FASTFLOAT_USE_UNLIKELY_ATTR 1 +#endif +#endif + +#ifdef FASTFLOAT_USE_UNLIKELY_ATTR +// 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 +// This is unfortunate. +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++20-extensions" +#endif #define fastfloat_unlikely(x) (x) [[unlikely]] +#ifdef __clang__ +#pragma clang diagnostic pop +#endif #elif defined(__GNUC__) || defined(__clang__) #define fastfloat_unlikely(x) (__builtin_expect(!!(x), 0)) #else diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index a844bc8..1071573 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -351,6 +351,10 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, // Slow path A (rare): > 19 significant digits. The no-span parse left the // mantissa un-truncated and skipped the span-based recompute; the cold helper // re-parses with spans and runs the full algorithm. +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++20-extensions" +#endif if fastfloat_unlikely (pns.too_many_digits) { return parse_number_slow_path(first, last, value, options, bjf); } @@ -371,6 +375,9 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, if fastfloat_unlikely (am.power2 < 0) { return parse_number_slow_path(first, last, value, options, bjf); } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif to_float(pns.negative, am, value); // Test for over/underflow. if ((pns.mantissa != 0 && am.mantissa == 0 && am.power2 == 0) || From b1fbfe932a3bb3626ecc70b923ce82833d706d3f Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sun, 7 Jun 2026 11:18:09 -0400 Subject: [PATCH 16/30] silencing -Wc++20-extensions at the point of use solely --- include/fast_float/float_common.h | 11 ----------- include/fast_float/parse_number.h | 9 ++++++++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 479febc..73a92fe 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -212,18 +212,7 @@ using parse_options = parse_options_t; #endif #ifdef FASTFLOAT_USE_UNLIKELY_ATTR -// 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 -// This is unfortunate. -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wc++20-extensions" -#endif #define fastfloat_unlikely(x) (x) [[unlikely]] -#ifdef __clang__ -#pragma clang diagnostic pop -#endif #elif defined(__GNUC__) || defined(__clang__) #define fastfloat_unlikely(x) (__builtin_expect(!!(x), 0)) #else diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index 1071573..5ec13e0 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -351,6 +351,11 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, // Slow path A (rare): > 19 significant digits. The no-span parse left the // mantissa un-truncated and skipped the span-based recompute; the cold helper // re-parses with spans and runs the full algorithm. + // +// 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 +// This is unfortunate. #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wc++20-extensions" @@ -358,7 +363,9 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, if fastfloat_unlikely (pns.too_many_digits) { return parse_number_slow_path(first, last, value, options, bjf); } - +#ifdef __clang__ +#pragma clang diagnostic pop +#endif answer.ec = std::errc(); // be optimistic answer.ptr = pns.lastmatch; From 29bd11571b0e83c411545b612c3e9eb7c0c7de5e Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sun, 7 Jun 2026 11:19:47 -0400 Subject: [PATCH 17/30] one too many --- include/fast_float/parse_number.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index 5ec13e0..7d338f3 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -363,9 +363,6 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, if fastfloat_unlikely (pns.too_many_digits) { return parse_number_slow_path(first, last, value, options, bjf); } -#ifdef __clang__ -#pragma clang diagnostic pop -#endif answer.ec = std::errc(); // be optimistic answer.ptr = pns.lastmatch; From e0b53eaf63c6d00e0725788ef1dbb759aa321d79 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sun, 7 Jun 2026 14:14:42 -0400 Subject: [PATCH 18/30] 8.2.7 --- CMakeLists.txt | 2 +- README.md | 6 +++--- include/fast_float/float_common.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f4ebafa..b369c44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.2.6 LANGUAGES CXX) +project(fast_float VERSION 8.2.7 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/README.md b/README.md index 193a977..6bf4d62 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,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.2.6 + GIT_TAG tags/v8.2.7 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -547,7 +547,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.2.6) + GIT_TAG v8.2.7) ``` ## Using as single header @@ -559,7 +559,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 73a92fe..4a4f576 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 2 -#define FASTFLOAT_VERSION_PATCH 6 +#define FASTFLOAT_VERSION_PATCH 7 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) From 23e245f2b3e9ec5bf5a4ccaced2e463a28b77534 Mon Sep 17 00:00:00 2001 From: Joao Paulo Magalhaes Date: Mon, 8 Jun 2026 12:24:39 +0100 Subject: [PATCH 19/30] Fix compile error in clang<10: fails on pragma -Wc++20-extensions This fixes a compile error in all clang versions lower than 10, triggered by the use of the pragma ignore with what is an unknown warning on those compiler versions: ``` /__w/ext/fast_float/include/fast_float/parse_number.h:361:34: error: unknown warning group '-Wc++20-extensions', ignored [-Werror,-Wunknown-pragmas] ``` The fix requires looking at __clang_major__, which is unfortunately different in Apple, so a version dispatch is required. --- include/fast_float/parse_number.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index 7d338f3..5d11dfa 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -358,7 +358,9 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, // This is unfortunate. #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 (pns.too_many_digits) { return parse_number_slow_path(first, last, value, options, bjf); From e8ec8e8f34bd6142efdbf4c714734dcff51ea9ac Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 8 Jun 2026 15:29:36 -0400 Subject: [PATCH 20/30] 8.2.8 --- CMakeLists.txt | 2 +- README.md | 6 +++--- include/fast_float/float_common.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b369c44..9f3f36f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.2.7 LANGUAGES CXX) +project(fast_float VERSION 8.2.8 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/README.md b/README.md index 6bf4d62..6523888 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,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.2.7 + GIT_TAG tags/v8.2.8 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -547,7 +547,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.2.7) + GIT_TAG v8.2.8) ``` ## Using as single header @@ -559,7 +559,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 4a4f576..66b95cc 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 2 -#define FASTFLOAT_VERSION_PATCH 7 +#define FASTFLOAT_VERSION_PATCH 8 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) From 8fe7a9405b9bfd4bc87e6439ca67ef932d97fca7 Mon Sep 17 00:00:00 2001 From: correctmost <134317971+correctmost@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:37:58 -0400 Subject: [PATCH 21/30] Remove an unreachable return statement The redundant statement was reported by cppcheck. --- tests/fixedwidthtest.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/fixedwidthtest.cpp b/tests/fixedwidthtest.cpp index 35c5231..510d2be 100644 --- a/tests/fixedwidthtest.cpp +++ b/tests/fixedwidthtest.cpp @@ -65,6 +65,4 @@ int main() { #endif std::cout << "All tests passed successfully." << std::endl; return EXIT_SUCCESS; - - return 0; -} \ No newline at end of file +} From 6ae691372fd9cc270f07375a38125ffd2afefa57 Mon Sep 17 00:00:00 2001 From: correctmost <134317971+correctmost@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:48:54 -0400 Subject: [PATCH 22/30] Remove an else if statement that is always false Commit b334317d added the same std::isnan(v) check as an earlier condition. The warning was reported by cppcheck. --- tests/long_exhaustive32_64.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/long_exhaustive32_64.cpp b/tests/long_exhaustive32_64.cpp index 8409588..ce6fba7 100644 --- a/tests/long_exhaustive32_64.cpp +++ b/tests/long_exhaustive32_64.cpp @@ -45,11 +45,6 @@ void all_32bit_values() { std::cerr << "I got " << std::hexfloat << result_value << " but I was expecting " << v << std::endl; abort(); - } else if (std::isnan(v)) { - if (!std::isnan(result_value)) { - std::cerr << "not nan" << buffer << std::endl; - abort(); - } } else if (result_value != v) { std::cerr << "no match ? " << buffer << std::endl; std::cout << "started with " << std::hexfloat << v << std::endl; From 82882b237d0af7c5dfd940b9eeeac2c2c351dc66 Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Wed, 10 Jun 2026 12:12:34 +0530 Subject: [PATCH 23/30] gate uint8/uint16 base-10 fast paths to single-byte code units --- include/fast_float/ascii_number.h | 6 ++-- tests/fast_int.cpp | 57 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index e431cbc..d748204 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -600,7 +600,8 @@ parse_int_string(UC const *p, UC const *pend, T &value, UC const *const start_digits = p; - FASTFLOAT_IF_CONSTEXPR17((std::is_same::value)) { + FASTFLOAT_IF_CONSTEXPR17((std::is_same::value && + sizeof(UC) == 1)) { if (base == 10) { const size_t len = (size_t)(pend - p); if (len == 0) { @@ -692,7 +693,8 @@ parse_int_string(UC const *p, UC const *pend, T &value, } } - FASTFLOAT_IF_CONSTEXPR17((std::is_same::value)) { + FASTFLOAT_IF_CONSTEXPR17((std::is_same::value && + sizeof(UC) == 1)) { if (base == 10) { const size_t len = size_t(pend - p); if (len == 0) { diff --git a/tests/fast_int.cpp b/tests/fast_int.cpp index 94e76fd..f7ec934 100644 --- a/tests/fast_int.cpp +++ b/tests/fast_int.cpp @@ -1295,6 +1295,63 @@ int main() { return EXIT_FAILURE; } } + // The uint8_t and uint16_t base-10 paths use a byte-oriented fast path. A + // wider code unit whose low byte is an ASCII digit (e.g. U+2131..U+2139) must + // not be mistaken for that digit. The generic path already rejects these for + // int; the fixed-width fast paths must agree. Lengths of 1..5 exercise both + // the uint8_t path and the 4-digit uint16_t SWAR path. + { + const std::u16string bad16[] = { + u"ℹ", + u"ℱℲ", + u"ℱℲℳ", + u"ℱℲℳℴ", + u"ℱℲℳℴℵ", + }; + const std::u32string bad32[] = { + U"ℹ", + U"ℱℲℳ", + U"ℱℲℳℴ", + U"ℱℲℳℴℵ", + }; + bool failed = false; + for (auto const &s : bad16) { + uint8_t r8 = 123; + auto a8 = fast_float::from_chars(s.data(), s.data() + s.size(), r8); + if (a8.ec == std::errc()) { + failed = true; + std::cerr << "Incorrectly parsed wide units as uint8_t " << unsigned(r8) + << "." << std::endl; + } + uint16_t r16 = 123; + auto a16 = fast_float::from_chars(s.data(), s.data() + s.size(), r16); + if (a16.ec == std::errc()) { + failed = true; + std::cerr << "Incorrectly parsed wide units as uint16_t " << r16 << "." + << std::endl; + } + } + for (auto const &s : bad32) { + uint8_t r8 = 123; + auto a8 = fast_float::from_chars(s.data(), s.data() + s.size(), r8); + if (a8.ec == std::errc()) { + failed = true; + std::cerr << "Incorrectly parsed wide units as uint8_t " << unsigned(r8) + << "." << std::endl; + } + uint16_t r16 = 123; + auto a16 = fast_float::from_chars(s.data(), s.data() + s.size(), r16); + if (a16.ec == std::errc()) { + failed = true; + std::cerr << "Incorrectly parsed wide units as uint16_t " << r16 << "." + << std::endl; + } + } + + if (failed) { + return EXIT_FAILURE; + } + } return EXIT_SUCCESS; } From 8e6edc8ad262ecb83e3b56323bca49c86b9e5c8b Mon Sep 17 00:00:00 2001 From: Joao Paulo Magalhaes Date: Wed, 10 Jun 2026 13:20:58 +0100 Subject: [PATCH 24/30] Fix compile error with gcc 9: use of [[unlikely]] --- include/fast_float/float_common.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 66b95cc..04b4a1c 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -207,7 +207,11 @@ using parse_options = parse_options_t; // to a no-op elsewhere (e.g. pre-C++20 MSVC, which has no equivalent hint). #ifdef __has_cpp_attribute #if __has_cpp_attribute(unlikely) >= 201803L -#define FASTFLOAT_USE_UNLIKELY_ATTR 1 +// g++-9 hits hits this branch, but then fails to compile +// [[unlikely]]. This happens only with g++-9. +#if !defined(__GNUC__) || (__GNUC__ != 9) +#define FASTFLOAT_USE_UNLIKELY_ATTR +#endif #endif #endif From 8234a896236a6c98ee66cf214b31ca15e24579ca Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Thu, 11 Jun 2026 20:29:24 -0400 Subject: [PATCH 25/30] 8.2.9 --- CMakeLists.txt | 2 +- README.md | 6 +++--- include/fast_float/ascii_number.h | 8 ++++---- include/fast_float/float_common.h | 2 +- tests/fast_int.cpp | 6 +----- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f3f36f..1e20b5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.2.8 LANGUAGES CXX) +project(fast_float VERSION 8.2.9 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/README.md b/README.md index 6523888..30b50b4 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,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.2.8 + GIT_TAG tags/v8.2.9 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -547,7 +547,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.2.8) + GIT_TAG v8.2.9) ``` ## Using as single header @@ -559,7 +559,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/ascii_number.h b/include/fast_float/ascii_number.h index d748204..b54c10b 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -600,8 +600,8 @@ parse_int_string(UC const *p, UC const *pend, T &value, UC const *const start_digits = p; - FASTFLOAT_IF_CONSTEXPR17((std::is_same::value && - sizeof(UC) == 1)) { + FASTFLOAT_IF_CONSTEXPR17( + (std::is_same::value && sizeof(UC) == 1)) { if (base == 10) { const size_t len = (size_t)(pend - p); if (len == 0) { @@ -693,8 +693,8 @@ parse_int_string(UC const *p, UC const *pend, T &value, } } - FASTFLOAT_IF_CONSTEXPR17((std::is_same::value && - sizeof(UC) == 1)) { + FASTFLOAT_IF_CONSTEXPR17( + (std::is_same::value && sizeof(UC) == 1)) { if (base == 10) { const size_t len = size_t(pend - p); if (len == 0) { diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 04b4a1c..66f04ac 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 2 -#define FASTFLOAT_VERSION_PATCH 8 +#define FASTFLOAT_VERSION_PATCH 9 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) diff --git a/tests/fast_int.cpp b/tests/fast_int.cpp index f7ec934..0a78faa 100644 --- a/tests/fast_int.cpp +++ b/tests/fast_int.cpp @@ -1302,11 +1302,7 @@ int main() { // the uint8_t path and the 4-digit uint16_t SWAR path. { const std::u16string bad16[] = { - u"ℹ", - u"ℱℲ", - u"ℱℲℳ", - u"ℱℲℳℴ", - u"ℱℲℳℴℵ", + u"ℹ", u"ℱℲ", u"ℱℲℳ", u"ℱℲℳℴ", u"ℱℲℳℴℵ", }; const std::u32string bad32[] = { U"ℹ", From 632cc97b5b45e6a13bbcdaf715fcff6fac289df6 Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Sat, 13 Jun 2026 21:20:45 +0530 Subject: [PATCH 26/30] detect uint64 overflow that wraps past min_safe in parse_int_string --- include/fast_float/ascii_number.h | 25 ++++++++++++-- tests/fast_int.cpp | 55 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index b54c10b..f895e9b 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -781,9 +781,28 @@ parse_int_string(UC const *p, UC const *pend, T &value, } // this check can be eliminated for all other types, but they will all require // a max_digits(base) equivalent - if (digit_count == max_digits && i < min_safe_u64(base)) { - answer.ec = std::errc::result_out_of_range; - return answer; + if (digit_count == max_digits) { + // A value that wrapped below the smallest max_digits-length value has + // certainly overflowed. + if (i < min_safe_u64(base)) { + answer.ec = std::errc::result_out_of_range; + return answer; + } + // i >= min_safe_u64(base) is still not proof that it fits: for any base + // whose max_digits-length range exceeds 2^64 (base 10 reaches ~5.4 * 2^64 + // at 20 digits) the accumulator can wrap a whole multiple of 2^64 and land + // back above min_safe, so the test above lets that overflow through. Re-run + // the parsed digits with a checked multiply-add to decide exactly. + uint64_t overflow_check = 0; + for (UC const *q = start_digits; q != p; ++q) { + uint8_t const digit = ch_to_digit(*q); + if (overflow_check > + (std::numeric_limits::max() - digit) / uint64_t(base)) { + answer.ec = std::errc::result_out_of_range; + return answer; + } + overflow_check = uint64_t(base) * overflow_check + digit; + } } // check other types overflow diff --git a/tests/fast_int.cpp b/tests/fast_int.cpp index 0a78faa..9762f96 100644 --- a/tests/fast_int.cpp +++ b/tests/fast_int.cpp @@ -821,6 +821,61 @@ int main() { ++base_unsigned; } + // unsigned out of range error base test, multi-wrap (64 bit) + // These values overflow uint64_t, but the accumulator wraps a whole multiple + // of 2^64 and lands back at or above the smallest max_digits-length value, so + // a single comparison against that bound does not catch the overflow. Bases + // 2, 4 and 16 are excluded because their max_digits-length range fits within + // a single 2^64 span. + std::vector const unsigned_multiwrap_base{ + 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36}; + std::vector const unsigned_multiwrap_base_test{ + "22222222222222222222222222222222222222222", + "4400000000000000000000000000", + "5555555555555555555555555", + "66666666666666666666666", + "7777777777777777777777", + "888888888888888888888", + "46893488147419103233", + "AAAAAAAAAAAAAAAAAAA", + "BBBBBBBBBBBBBBBBBB", + "427772311192C9BAAB", + "DDDDDDDDDDDDDDDDD", + "532C82996D3A44919", + "GGGGGGGGGGGGGGGG", + "HHHHHHHHHHHHHHHH", + "3835GEGDF36622EG", + "JJJJJJJJJJJJJJJ", + "KKKKKKKKKKKKKKK", + "LLLLLLLLLLLLLLL", + "444BGHB4EG5DA2D", + "NNNNNNNNNNNNNN", + "JE5H4MNDLJGNLO", + "PPPPPPPPPPPPPP", + "QQQQQQQQQQQQQQ", + "RRRRRRRRRRRRRR", + "4H7QS52310IHQK", + "TTTTTTTTTTTTTT", + "UUUUUUUUUUUUU", + "VVVVVVVVVVVVV", + "WWWWWWWWWWWWW", + "XXXXXXXXXXXXX", + "YYYYYYYYYYYYY", + "6U831JL976P6O"}; + + for (std::size_t i = 0; i < unsigned_multiwrap_base_test.size(); ++i) { + auto const &f = unsigned_multiwrap_base_test[i]; + uint64_t result; + auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result, + unsigned_multiwrap_base[i]); + if (answer.ec != std::errc::result_out_of_range) { + std::cerr << "expected error for should be 'result_out_of_range': \"" << f + << "\"" << std::endl; + return EXIT_FAILURE; + } + } + // just within range base test (64 bit) std::vector const int_within_range_base_test{ "111111111111111111111111111111111111111111111111111111111111111", From a7249f86ededbe739dcb29f13b31144130ceaba7 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sat, 13 Jun 2026 21:22:10 -0400 Subject: [PATCH 27/30] replace checked re-parse with O(1) simdjson-style overflow check The previous commit detects multi-wrap u64 overflow at the max_digits boundary by re-parsing the digits through a checked multiply-add loop (O(max_digits)). Replace that with the constant-time check used in simdjson: the leading digit plus a single threshold comparison. For a max_digits-length value, min_safe_u64(base) == base^(max_digits-1) is the smallest such value and also the width of each leading-digit band [d*ms, (d+1)*ms). Since that width is < 2^64, the only band that can straddle 2^64 is d == dmax (the largest leading digit that still fits), and there it straddles at most once, so a single threshold dmax*ms separates wrapped from non-wrapped values. A leading digit above dmax always overflows; below dmax always fits. dmax and the threshold derive from the existing min_safe_u64 table, so no new tables are needed and dmax*ms cannot itself overflow. Add a programmatic, self-verifying test for parse_int_string overflow detection covering bases 2..36, complementing the hand-picked strings added earlier. Every generated input is cross-checked against an independent trusted oracle (a plain 64-bit checked multiply-add); on success the parsed value is also compared exactly and full consumption of the input is asserted. Per base it exercises: - an exact-boundary sweep of the 64 values straddling 2^64 (UINT64_MAX-31 .. 2^64+31), built by walking the digit string; - UINT64_MAX, 2^64 and the all-max-digit value, each also with leading zeros; - random max_digits-length values across every leading digit, with the heaviest sampling on the lead == dmax band that straddles 2^64, and full coverage of lead > dmax (the multi-wrap region the naive min_safe check accepted by mistake); - max_digits-1 (never overflows) and max_digits+1 (always overflows). A small signed (int64_t) section checks the exact INT64_MIN/INT64_MAX limits round-trip and that INT64_MAX+1 / INT64_MIN-1 are rejected in every base. --- include/fast_float/ascii_number.h | 35 ++-- tests/fast_int.cpp | 274 ++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+), 18 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index f895e9b..e6ead45 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -782,27 +782,26 @@ parse_int_string(UC const *p, UC const *pend, T &value, // this check can be eliminated for all other types, but they will all require // a max_digits(base) equivalent if (digit_count == max_digits) { - // A value that wrapped below the smallest max_digits-length value has - // certainly overflowed. - if (i < min_safe_u64(base)) { + // At the max_digits boundary the accumulator `i` may have wrapped around + // 2^64. A plain `i < min_safe_u64(base)` test is not sufficient: for any + // base whose max_digits-length range exceeds 2^64 (base 10 reaches + // ~5.4 * 2^64 at 20 digits) the value can wrap a whole multiple of 2^64 and + // land back above min_safe, slipping through. Decide exactly in O(1) using + // the leading digit, following the approach used in simdjson: + // ms == min_safe_u64(base) == base^(max_digits-1), the smallest + // max_digits-length value. + // dmax == the largest leading digit whose number can still fit in u64. + // The leading-digit band [d*ms, (d+1)*ms) has width ms < 2^64, so within + // the single band where d == dmax the value straddles 2^64 at most once, + // and a single threshold separates wrapped from non-wrapped values. A + // leading digit above dmax always overflows; below dmax always fits. + uint64_t const ms = min_safe_u64(base); + uint64_t const dmax = (std::numeric_limits::max)() / ms; + uint64_t const lead = ch_to_digit(*start_digits); + if (lead > dmax || (lead == dmax && i < dmax * ms)) { answer.ec = std::errc::result_out_of_range; return answer; } - // i >= min_safe_u64(base) is still not proof that it fits: for any base - // whose max_digits-length range exceeds 2^64 (base 10 reaches ~5.4 * 2^64 - // at 20 digits) the accumulator can wrap a whole multiple of 2^64 and land - // back above min_safe, so the test above lets that overflow through. Re-run - // the parsed digits with a checked multiply-add to decide exactly. - uint64_t overflow_check = 0; - for (UC const *q = start_digits; q != p; ++q) { - uint8_t const digit = ch_to_digit(*q); - if (overflow_check > - (std::numeric_limits::max() - digit) / uint64_t(base)) { - answer.ec = std::errc::result_out_of_range; - return answer; - } - overflow_check = uint64_t(base) * overflow_check + digit; - } } // check other types overflow diff --git a/tests/fast_int.cpp b/tests/fast_int.cpp index 9762f96..52ad5a6 100644 --- a/tests/fast_int.cpp +++ b/tests/fast_int.cpp @@ -17,7 +17,10 @@ #include #include #include +#include #include +#include +#include #include "fast_float/fast_float.h" #include @@ -1404,6 +1407,277 @@ int main() { } } + // Comprehensive, oracle-checked u64 overflow detection across every base. + // + // The accumulator in parse_int_string is allowed to overflow and the result + // is validated afterwards. At the max_digits boundary a value can wrap one or + // more whole multiples of 2^64 (a 20-digit base-10 number reaches ~5.4*2^64), + // so the boundary check must be exact. This section validates from_chars for + // bases 2..36 against an independent, trusted oracle: a plain 64-bit checked + // multiply-add. It hammers the single leading-digit band that straddles 2^64 + // (where wrapped and non-wrapped values are hardest to tell apart) and also + // covers max_digits-1 (always in range) and max_digits+1 (always overflow). + { + auto digit_to_char = [](int d) -> char { + return d < 10 ? char('0' + d) : char('A' + (d - 10)); + }; + auto char_to_digit = [](char c) -> int { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'Z') { + return c - 'A' + 10; + } + return c - 'a' + 10; + }; + // Trusted oracle: parse `s` in `base` with a checked 64-bit multiply-add. + // Returns true on u64 overflow; otherwise writes the value to `out`. + auto oracle = [&](std::string const &s, int base, uint64_t &out) -> bool { + uint64_t v = 0; + for (char c : s) { + uint64_t const d = uint64_t(char_to_digit(c)); + if (v > (UINT64_MAX - d) / uint64_t(base)) { + return true; + } + v = uint64_t(base) * v + d; + } + out = v; + return false; + }; + auto to_base = [&](uint64_t v, int base) -> std::string { + if (v == 0) { + return "0"; + } + std::string s; + while (v != 0) { + s += digit_to_char(int(v % uint64_t(base))); + v /= uint64_t(base); + } + std::reverse(s.begin(), s.end()); + return s; + }; + // Add one (in base `base`) to the digit string `s`, carrying as needed. + auto increment = [&](std::string s, int base) -> std::string { + int carry = 1; + for (std::size_t k = s.size(); k-- > 0 && carry != 0;) { + int const d = char_to_digit(s[k]) + carry; + carry = d / base; + s[k] = digit_to_char(d % base); + } + if (carry != 0) { + s.insert(s.begin(), digit_to_char(carry)); + } + return s; + }; + + // Subtract one (in base `base`) from a non-zero, non-negative string. + auto decrement = [&](std::string s, int base) -> std::string { + int borrow = 1; + for (std::size_t k = s.size(); k-- > 0 && borrow != 0;) { + int d = char_to_digit(s[k]) - borrow; + borrow = d < 0 ? 1 : 0; + if (d < 0) { + d += base; + } + s[k] = digit_to_char(d); + } + std::size_t lead = s.find_first_not_of('0'); // drop any leading zero + return lead == std::string::npos ? "0" : s.substr(lead); + }; + + std::mt19937_64 rng(0xC0FFEEULL); + long long checked = 0; + auto verify = [&](std::string const &s, int base) -> bool { + uint64_t expected = 0; + bool const ov = oracle(s, base, expected); + uint64_t result = 0xDEADBEEFULL; + auto answer = + fast_float::from_chars(s.data(), s.data() + s.size(), result, base); + ++checked; + if (ov) { + if (answer.ec != std::errc::result_out_of_range) { + std::cerr << "base " << base + << ": expected result_out_of_range for \"" << s << "\"" + << std::endl; + return false; + } + } else { + if (answer.ec != std::errc()) { + std::cerr << "base " << base << ": unexpected error for \"" << s + << "\"" << std::endl; + return false; + } + if (result != expected) { + std::cerr << "base " << base << ": \"" << s << "\" -> " << result + << ", expected " << expected << std::endl; + return false; + } + if (answer.ptr != s.data() + s.size()) { + std::cerr << "base " << base << ": did not consume all of \"" << s + << "\"" << std::endl; + return false; + } + } + return true; + }; + // Leading zeros are stripped before the digit count, so the outcome must be + // unchanged. Checked only on hand-picked values (it exercises shared code). + auto verify_zeros = [&](std::string const &digits, int base) -> bool { + return verify(digits, base) && verify("0" + digits, base) && + verify(std::string(40, '0') + digits, base); + }; + auto random_tail = [&](std::string &s, int n, int base) { + for (int k = 0; k < n; ++k) { + // bias toward the extremes (0 and base-1) to hit boundaries often + std::uint64_t const r = rng(); + int const mode = int(r % 4); + int const dig = mode == 0 ? 0 + : mode == 1 ? base - 1 + : int((r >> 2) % std::uint64_t(base)); + s += digit_to_char(dig); + } + }; + + for (int base = 2; base <= 36; ++base) { + // M = max number of base-`base` digits a u64 can hold. + std::string const maxstr = to_base(UINT64_MAX, base); + int const M = int(maxstr.size()); + // b^(M-1): smallest M-digit value, and width of each leading-digit band. + uint64_t bM1 = 1; + for (int k = 0; k < M - 1; ++k) { + bM1 *= uint64_t(base); + } + int const dmax = int(UINT64_MAX / bM1); // largest leading digit that fits + + // Exact-boundary sweep straddling 2^64 (the hardest transition): the + // 64 values UINT64_MAX-31 .. UINT64_MAX (in range) and 2^64 .. 2^64+31 + // (overflow), built by walking the digit string up and down. + std::string below = maxstr, above = increment(maxstr, base); + for (int k = 0; k < 32; ++k) { + if (!verify(below, base) || !verify(above, base)) { + return EXIT_FAILURE; + } + below = decrement(below, base); + above = increment(above, base); + } + // Hand-picked values, also checked with leading zeros. + std::string const allmax(std::size_t(M), digit_to_char(base - 1)); + if (!verify_zeros(maxstr, base) || // largest in-range value + !verify_zeros(increment(maxstr, base), base) || // smallest overflow + !verify_zeros(allmax, base)) { // largest M-digit (multi-wrap) + return EXIT_FAILURE; + } + + // Randomized M-digit values across every leading digit. Bands with + // lead > dmax always overflow (this is where the naive min_safe check + // wrongly accepted multi-wrap values); lead < dmax always fits; lead == + // dmax straddles 2^64 and gets the heaviest sampling. + for (int lead = 1; lead < base; ++lead) { + int const trials = lead == dmax ? 4000 : 300; + for (int trial = 0; trial < trials; ++trial) { + std::string s(1, digit_to_char(lead)); + random_tail(s, M - 1, base); + if (!verify(s, base)) { + return EXIT_FAILURE; + } + } + } + // max_digits-1 digits never overflow; max_digits+1 digits always do. + for (int trial = 0; trial < 500; ++trial) { + std::string shorts(1, + digit_to_char(1 + int(rng() % uint64_t(base - 1)))); + random_tail(shorts, M - 2, base); + std::string longs(1, + digit_to_char(1 + int(rng() % uint64_t(base - 1)))); + random_tail(longs, M, base); + if (!verify(shorts, base) || !verify(longs, base)) { + return EXIT_FAILURE; + } + } + } + if (checked < 100000) { + std::cerr << "overflow sweep ran too few cases: " << checked << std::endl; + return EXIT_FAILURE; + } + } + + // Signed (int64_t) boundary: every value that overflows u64 also overflows + // i64, and the exact i64 limits must parse. Reuses the oracle indirectly via + // hand-built extremes per base. + { + auto digit_to_char = [](int d) -> char { + return d < 10 ? char('0' + d) : char('A' + (d - 10)); + }; + auto to_base_signed = [&](int64_t value, int base) -> std::string { + // value may be INT64_MIN; accumulate magnitude in u64 to avoid UB. + bool const neg = value < 0; + uint64_t mag = neg ? (~uint64_t(value) + 1) : uint64_t(value); + std::string s; + if (mag == 0) { + s = "0"; + } + while (mag != 0) { + s += digit_to_char(int(mag % uint64_t(base))); + mag /= uint64_t(base); + } + if (neg) { + s += '-'; + } + std::reverse(s.begin(), s.end()); + return s; + }; + for (int base = 2; base <= 36; ++base) { + struct { + int64_t v; + } const limits[] = {{INT64_MAX}, {INT64_MIN}, {0}, {-1}, {1}}; + + for (auto const &lim : limits) { + std::string const s = to_base_signed(lim.v, base); + int64_t result = 123; + auto answer = + fast_float::from_chars(s.data(), s.data() + s.size(), result, base); + if (answer.ec != std::errc() || result != lim.v) { + std::cerr << "base " << base << ": signed limit \"" << s + << "\" failed to round-trip (got " << result << ")" + << std::endl; + return EXIT_FAILURE; + } + } + // Increment a non-negative magnitude string (in `base`) by one. + auto inc_mag = [&](std::string m) -> std::string { + int carry = 1; + for (std::size_t k = m.size(); k-- > 0 && carry != 0;) { + int d = (m[k] >= '0' && m[k] <= '9') ? m[k] - '0' + : (m[k] >= 'A' && m[k] <= 'Z') ? m[k] - 'A' + 10 + : m[k] - 'a' + 10; + d += carry; + carry = d / base; + m[k] = digit_to_char(d % base); + } + if (carry != 0) { + m.insert(m.begin(), digit_to_char(carry)); + } + return m; + }; + // INT64_MAX + 1 (= 2^63) overflows a positive int64_t. + // INT64_MIN - 1 (= -(2^63 + 1)) overflows a negative int64_t. + // Note that -(2^63) == INT64_MIN is in range and is covered above. + std::string const max_mag = to_base_signed(INT64_MAX, base); // 2^63 - 1 + std::string const over = inc_mag(max_mag); // 2^63 + std::string const under = "-" + inc_mag(over); // -(2^63 + 1) + for (std::string const &s : {over, under}) { + int64_t result = 123; + auto answer = + fast_float::from_chars(s.data(), s.data() + s.size(), result, base); + if (answer.ec != std::errc::result_out_of_range) { + std::cerr << "base " << base << ": expected result_out_of_range for " + << "signed \"" << s << "\"" << std::endl; + return EXIT_FAILURE; + } + } + } + } + return EXIT_SUCCESS; } #else From fd970ab05ed79f9a8322f568cd245fac241c4f70 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sat, 13 Jun 2026 21:41:53 -0400 Subject: [PATCH 28/30] updating visual studio --- .github/workflows/vs17-arm-ci.yml | 4 ++-- .github/workflows/vs17-ci.yml | 8 ++++---- .github/workflows/vs17-clang-ci.yml | 8 ++++---- .github/workflows/vs17-cxx20.yml | 8 ++++---- tests/fast_int.cpp | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/vs17-arm-ci.yml b/.github/workflows/vs17-arm-ci.yml index 9eef012..e1c6f77 100644 --- a/.github/workflows/vs17-arm-ci.yml +++ b/.github/workflows/vs17-arm-ci.yml @@ -10,8 +10,8 @@ jobs: fail-fast: false matrix: include: - - {gen: Visual Studio 17 2022, arch: ARM64, cfg: Release} - - {gen: Visual Studio 17 2022, arch: ARM64, cfg: Debug} + - {gen: Visual Studio 18 2026, arch: ARM64, cfg: Release} + - {gen: Visual Studio 18 2026, arch: ARM64, cfg: Debug} steps: - name: checkout uses: actions/checkout@v6.0.2 diff --git a/.github/workflows/vs17-ci.yml b/.github/workflows/vs17-ci.yml index 39a5bd4..cdee064 100644 --- a/.github/workflows/vs17-ci.yml +++ b/.github/workflows/vs17-ci.yml @@ -10,10 +10,10 @@ jobs: fail-fast: false matrix: include: - - {gen: Visual Studio 17 2022, arch: Win32, cfg: Release} - #- {gen: Visual Studio 17 2022, arch: Win32, cfg: Debug} - - {gen: Visual Studio 17 2022, arch: x64, cfg: Release} - - {gen: Visual Studio 17 2022, arch: x64, cfg: Debug} + - {gen: Visual Studio 18 2026, arch: Win32, cfg: Release} + #- {gen: Visual Studio 18 2026, arch: Win32, cfg: Debug} + - {gen: Visual Studio 18 2026, arch: x64, cfg: Release} + - {gen: Visual Studio 18 2026, arch: x64, cfg: Debug} steps: - name: checkout uses: actions/checkout@v6.0.2 diff --git a/.github/workflows/vs17-clang-ci.yml b/.github/workflows/vs17-clang-ci.yml index 25a54b8..27fc268 100644 --- a/.github/workflows/vs17-clang-ci.yml +++ b/.github/workflows/vs17-clang-ci.yml @@ -10,10 +10,10 @@ jobs: fail-fast: false matrix: include: - - {gen: Visual Studio 17 2022, arch: Win32, cfg: Release} - - {gen: Visual Studio 17 2022, arch: Win32, cfg: Debug} - - {gen: Visual Studio 17 2022, arch: x64, cfg: Release} - - {gen: Visual Studio 17 2022, arch: x64, cfg: Debug} + - {gen: Visual Studio 18 2026, arch: Win32, cfg: Release} + - {gen: Visual Studio 18 2026, arch: Win32, cfg: Debug} + - {gen: Visual Studio 18 2026, arch: x64, cfg: Release} + - {gen: Visual Studio 18 2026, arch: x64, cfg: Debug} steps: - name: checkout uses: actions/checkout@v6.0.2 diff --git a/.github/workflows/vs17-cxx20.yml b/.github/workflows/vs17-cxx20.yml index dda7afc..bc9e11e 100644 --- a/.github/workflows/vs17-cxx20.yml +++ b/.github/workflows/vs17-cxx20.yml @@ -10,10 +10,10 @@ jobs: fail-fast: false matrix: include: - - {gen: Visual Studio 17 2022, arch: Win32, cfg: Release} - - {gen: Visual Studio 17 2022, arch: Win32, cfg: Debug} - - {gen: Visual Studio 17 2022, arch: x64, cfg: Release} - - {gen: Visual Studio 17 2022, arch: x64, cfg: Debug} + - {gen: Visual Studio 18 2026, arch: Win32, cfg: Release} + - {gen: Visual Studio 18 2026, arch: Win32, cfg: Debug} + - {gen: Visual Studio 18 2026, arch: x64, cfg: Release} + - {gen: Visual Studio 18 2026, arch: x64, cfg: Debug} steps: - name: checkout uses: actions/checkout@v6.0.2 diff --git a/tests/fast_int.cpp b/tests/fast_int.cpp index 52ad5a6..10e7dcf 100644 --- a/tests/fast_int.cpp +++ b/tests/fast_int.cpp @@ -1614,7 +1614,7 @@ int main() { uint64_t mag = neg ? (~uint64_t(value) + 1) : uint64_t(value); std::string s; if (mag == 0) { - s = "0"; + s += '0'; } while (mag != 0) { s += digit_to_char(int(mag % uint64_t(base))); From 34164f547b7df3f5d794ff67e9f885c36819ebfc Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Sun, 14 Jun 2026 09:52:47 -0400 Subject: [PATCH 29/30] 8.2.10 --- CMakeLists.txt | 2 +- README.md | 6 +++--- include/fast_float/float_common.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e20b5a..1ec6142 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) -project(fast_float VERSION 8.2.9 LANGUAGES CXX) +project(fast_float VERSION 8.2.10 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/README.md b/README.md index 30b50b4..d52fefe 100644 --- a/README.md +++ b/README.md @@ -531,7 +531,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.2.9 + GIT_TAG tags/v8.2.10 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -547,7 +547,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.2.9) + GIT_TAG v8.2.10) ``` ## Using as single header @@ -559,7 +559,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 66f04ac..4055137 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 2 -#define FASTFLOAT_VERSION_PATCH 9 +#define FASTFLOAT_VERSION_PATCH 10 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) From c539b5399cb382f5604de21b950949311ffc6faf Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Thu, 18 Jun 2026 19:51:56 +0530 Subject: [PATCH 30/30] guard is_space against negative signed code units --- include/fast_float/float_common.h | 6 +++++- tests/wide_char_test.cpp | 27 ++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index 4055137..de39fdd 100644 --- a/include/fast_float/float_common.h +++ b/include/fast_float/float_common.h @@ -1223,7 +1223,11 @@ template constexpr bool space_lut::value[]; #endif template constexpr bool is_space(UC c) { - return c < 256 && space_lut<>::value[uint8_t(c)]; + // wchar_t and char can be signed, so a negative code unit slips past a plain + // `c < 256` and then indexes the table by its truncated low byte. Compare as + // unsigned, matching the care taken in ch_to_digit. + using UnsignedUC = typename std::make_unsigned::type; + return static_cast(c) < 256 && space_lut<>::value[uint8_t(c)]; } template static constexpr uint64_t int_cmp_zeros() { diff --git a/tests/wide_char_test.cpp b/tests/wide_char_test.cpp index 3b6ccd6..f31e1ba 100644 --- a/tests/wide_char_test.cpp +++ b/tests/wide_char_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include bool tester(std::string s, double expected, fast_float::chars_format fmt = fast_float::chars_format::general) { @@ -46,8 +47,32 @@ bool test_nan() { return tester("nan", std::numeric_limits::quiet_NaN()); } +// A wide code unit whose low byte is an ASCII space (0x20) but whose full value +// is not whitespace must not be skipped by skip_white_space. When wchar_t is +// signed such a unit can be negative, which used to slip past the range guard +// in is_space and get treated as a space. +bool test_non_space_with_space_low_byte() { + if (!std::is_signed::value) { + return true; // only reproducible where wchar_t is signed + } + std::wstring input = L" 42"; + // 0x...FF20: low byte 0x20, high bits set, so the value is negative. + input[0] = static_cast(~static_cast(0xFF) | 0x20u); + double result; + auto answer = + fast_float::from_chars(input.data(), input.data() + input.size(), result, + fast_float::chars_format::general | + fast_float::chars_format::skip_white_space); + if (answer.ec == std::errc()) { + std::cerr << "leading non-space code unit must not be skipped\n"; + return false; + } + return true; +} + int main() { - if (test_minus() && test_plus() && test_space() && test_nan()) { + if (test_minus() && test_plus() && test_space() && test_nan() && + test_non_space_with_space_low_byte()) { std::cout << "all ok" << std::endl; return EXIT_SUCCESS; }