diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..2bbc472 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,64 @@ +name: Deploy GitHub Pages + +on: + push: + branches: [main] + paths: + - "docs/**" + - ".github/workflows/pages.yml" + - "CMakeLists.txt" + release: + types: [published] + workflow_dispatch: + +permissions: + contents: write # Changed: need write to push to gh-pages + pages: read # No longer needed for deploy-pages + id-token: none + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + name: Deploy to gh-pages + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Resolve latest release version + id: version + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + tag="$(gh release view --repo ${{ github.repository }} --json tagName --jq .tagName 2>/dev/null || true)" + if [ -z "${tag:-}" ]; then + tag="$(git tag --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" + fi + if [ -z "${tag:-}" ]; then + tag="v$(grep -E 'project\(fast_float VERSION' CMakeLists.txt | sed -E 's/.*VERSION ([0-9.]+).*/\1/')" + fi + version="${tag#v}" + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "Resolved version: ${version}" + + - name: Substitute version into HTML + run: | + version='${{ steps.version.outputs.version }}' + find docs -type f \( -name '*.html' -o -name '*.md' -o -name '*.css' -o -name '*.js' \) \ + -exec sed -i "s/{{VERSION}}/${version}/g" {} + + + - name: Deploy to gh-pages branch + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs + clean: true + commit-message: "Deploy docs for version ${{ steps.version.outputs.version }}" \ No newline at end of file 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/CMakeLists.txt b/CMakeLists.txt index e7cedc2..1ec6142 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.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 5998f8f..4fd8496 100644 --- a/README.md +++ b/README.md @@ -548,7 +548,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), @@ -562,15 +561,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 ------ @@ -648,7 +638,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.10 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(fast_float) @@ -664,7 +654,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.10) ``` ## Using as single header @@ -676,7 +666,7 @@ if desired as described in the command line help. You may directly download automatically generated single-header files: - + ## Benchmarking 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..f9785cf --- /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

+ +
+
+
+ + + + + + + + + + diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index d25b601..61b2b94 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -248,6 +248,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 : uint_fast8_t { @@ -304,10 +319,18 @@ report_parse_error(parsed_number_string_t &answer, UC const *p, // 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 const options) noexcept { + parse_options_t const options, + bool store_spans = true) noexcept { parsed_number_string_t answer{}; // so dereference without checks FASTFLOAT_ASSUME(p < pend); @@ -338,24 +361,54 @@ parse_number_string(UC const *p, UC const *pend, } } #endif - auto const *const start_digits = p; - while ((p != pend) && is_integer(*p)) { - // a multiplication by 10 is cheaper than an arbitrary integer - // multiplication - answer.mantissa = static_cast( - answer.mantissa * 10 + - static_cast( - *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)) { + answer.mantissa = static_cast(*p - UC('0')); ++p; + if ((p != pend) && is_integer(*p)) { + answer.mantissa = static_cast( + answer.mantissa * 10 + + static_cast(*p - UC('0'))); + ++p; + if ((p != pend) && is_integer(*p)) { + answer.mantissa = static_cast( + answer.mantissa * 10 + + static_cast(*p - UC('0'))); + ++p; + if ((p != pend) && is_integer(*p)) { + answer.mantissa = static_cast( + answer.mantissa * 10 + + static_cast(*p - UC('0'))); + ++p; + if ((p != pend) && is_integer(*p)) { + answer.mantissa = static_cast( + answer.mantissa * 10 + + static_cast(*p - UC('0'))); + ++p; + while ((p != pend) && is_integer(*p)) { + // a multiplication by 10 is cheaper than an arbitrary integer + // multiplication + answer.mantissa = static_cast( + answer.mantissa * 10 + + static_cast( + *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); + if (store_spans) { + answer.integer = span(start_digits, size_t(digit_count)); } - - auto const *const end_of_integer_part = p; - auto digit_count = static_cast(end_of_integer_part - start_digits); - answer.integer = span(start_digits, digit_count); - // We have now parsed the integer part of the mantissa. - #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) { // at least 1 digit in integer part, without leading zeros @@ -371,7 +424,8 @@ parse_number_string(UC const *p, UC const *pend, #endif // We can now parse the fraction part of the mantissa. - if ((p != pend) && (*p == options.decimal_point)) { + bool const has_decimal_point = (p != pend) && (*p == options.decimal_point); + if (has_decimal_point) { ++p; auto const *const before = p; // can occur at most twice without overflowing, but let it occur more, since @@ -380,14 +434,16 @@ parse_number_string(UC const *p, UC const *pend, while ((p != pend) && is_integer(*p)) { auto const digit = uint8_t(*p - UC('0')); + ++p; answer.mantissa = static_cast( answer.mantissa * 10 + digit); // in rare cases, this will overflow, but that's ok - ++p; } answer.exponent = static_cast(before - p); - answer.fraction = - span(before, static_cast(p - before)); + if (store_spans) { + answer.fraction = + span(before, static_cast(p - before)); + } digit_count -= static_cast(answer.exponent); #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) { @@ -500,35 +556,41 @@ parse_number_string(UC const *p, UC const *pend, // We have to check if number has more than 19 significant digits. 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. - answer.mantissa = 0; - p = answer.integer.ptr; - UC const *int_end = p + answer.integer.len(); - constexpr am_mant_t minimal_nineteen_digit_integer{1000000000000000000}; - while ((p != int_end) && - (answer.mantissa < minimal_nineteen_digit_integer)) { - answer.mantissa = - answer.mantissa * 10 + static_cast(*p - UC('0')); - ++p; - } - if (answer.mantissa >= minimal_nineteen_digit_integer) { - // We have a big integers, so skip the fraction part completely. - answer.exponent = am_pow_t(end_of_integer_part - p) + exp_number; - } else { - // We have a value with a significant fractional component. - p = answer.fraction.ptr; - UC const *const frac_end = p + answer.fraction.len(); - while ((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. + answer.mantissa = 0; + p = answer.integer.ptr; + UC const *int_end = p + answer.integer.len(); + constexpr am_mant_t minimal_nineteen_digit_integer{1000000000000000000}; + while ((p != int_end) && (answer.mantissa < minimal_nineteen_digit_integer)) { - answer.mantissa = static_cast( - answer.mantissa * 10 + static_cast(*p - UC('0'))); + answer.mantissa = + answer.mantissa * 10 + static_cast(*p - UC('0')); ++p; } - answer.exponent = am_pow_t(answer.fraction.ptr - p) + exp_number; + if (answer.mantissa >= minimal_nineteen_digit_integer) { + // We have a big integers, so skip the fraction part completely. + answer.exponent = am_pow_t(end_of_integer_part - p) + exp_number; + } else { + // We have a value with a significant fractional component. + p = answer.fraction.ptr; + UC const *const frac_end = p + answer.fraction.len(); + while ((p != frac_end) && + (answer.mantissa < minimal_nineteen_digit_integer)) { + answer.mantissa = static_cast( + answer.mantissa * 10 + static_cast(*p - UC('0'))); + ++p; + } + answer.exponent = am_pow_t(answer.fraction.ptr - p) + exp_number; + } + // We now corrected both exponent and mantissa, to a truncated value } - // We now corrected both exponent and mantissa, to a truncated value } } @@ -576,7 +638,8 @@ parse_int_string(UC const *p, UC const *pend, T &value, auto const *const start_digits = p; - FASTFLOAT_IF_CONSTEXPR17((std::is_same::value)) { + FASTFLOAT_IF_CONSTEXPR17((std::is_same::value) && + sizeof(UC) == 1) { if (options.base == 10) { auto const len = static_cast(pend - p); if (len == 0) { @@ -595,6 +658,7 @@ parse_int_string(UC const *p, UC const *pend, T &value, if (len >= sizeof(uint32_t)) { digits = read_chars_to_unsigned(p); + } else { uint32_t const b0 = static_cast(p[0]); uint32_t const b1 = (len > 1) ? static_cast(p[1]) : 0x00u; @@ -602,9 +666,6 @@ parse_int_string(UC const *p, UC const *pend, T &value, uint32_t const b3 = 0x00u; digits = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); } -#if FASTFLOAT_IS_BIG_ENDIAN - digits = byteswap(digits); -#endif uint32_t const magic = ((digits + 0x46464646u) | (digits - 0x30303030u)) & 0x80808080u; @@ -617,10 +678,9 @@ parse_int_string(UC const *p, UC const *pend, T &value, answer.ec = std::errc(); answer.ptr = p; return answer; - } else { - answer.ec = std::errc::invalid_argument; - answer.ptr = first; } + answer.ec = std::errc::invalid_argument; + answer.ptr = first; return answer; } if (nd > 3) { @@ -655,7 +715,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 (options.base == 10) { const auto len = static_cast(pend - p); if (len == 0) { @@ -663,10 +724,10 @@ parse_int_string(UC const *p, UC const *pend, T &value, value = 0; answer.ec = std::errc(); answer.ptr = p; - } else { - answer.ec = std::errc::invalid_argument; - answer.ptr = first; + return answer; } + answer.ec = std::errc::invalid_argument; + answer.ptr = first; return answer; } @@ -678,7 +739,7 @@ parse_int_string(UC const *p, UC const *pend, T &value, v = v * 10 + static_cast(p[4] - '0'); if (len >= 6 && is_integer(p[5])) { answer.ec = std::errc::result_out_of_range; - const auto *q = p + 5; + const UC *q = p + 5; while (q != pend && is_integer(*q)) { ++q; } @@ -726,10 +787,11 @@ parse_int_string(UC const *p, UC const *pend, T &value, value = 0; answer.ec = std::errc(); answer.ptr = p; - } else { - answer.ec = std::errc::invalid_argument; - answer.ptr = first; + return answer; } + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + return answer; } @@ -743,9 +805,27 @@ 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(options.base)) { - answer.ec = std::errc::result_out_of_range; - return answer; + if (digit_count == max_digits) { + // 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(options.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; + } } // check other types overflow diff --git a/include/fast_float/fast_float.h b/include/fast_float/fast_float.h index 4bb8544..9aac5e4 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 diff --git a/include/fast_float/float_common.h b/include/fast_float/float_common.h index a008b57..f832b53 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 10 #define FASTFLOAT_STRINGIZE_IMPL(x) #x #define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) @@ -227,6 +227,32 @@ using parse_options = parse_options_t; #define fastfloat_really_inline inline __attribute__((always_inline)) #endif +// 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 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). +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(unlikely) >= 201803L +// 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 + +#ifdef FASTFLOAT_USE_UNLIKELY_ATTR +#define fastfloat_unlikely(x) (x) [[unlikely]] +#elif defined(__GNUC__) || defined(__clang__) +#define fastfloat_unlikely(x) (__builtin_expect(!!(x), 0)) +#else +#define fastfloat_unlikely(x) (x) +#endif + #ifndef FASTFLOAT_ASSERT #define FASTFLOAT_ASSERT(x) \ { ((void)(x)); } @@ -1305,7 +1331,11 @@ template constexpr uint8_t 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)]; } #endif diff --git a/include/fast_float/parse_number.h b/include/fast_float/parse_number.h index 1ebdf1e..d37e155 100644 --- a/include/fast_float/parse_number.h +++ b/include/fast_float/parse_number.h @@ -318,6 +318,30 @@ from_chars_advanced(parsed_number_string_t const &pns, T &value) noexcept { return answer; } +// 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_CONSTEXPR20 from_chars_result_t +parse_number_slow_path(UC const *first, UC const *last, T &value, + parse_options_t options +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + , + bool bjf +#endif + ) noexcept { + parsed_number_string_t pns = +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + bjf ? parse_number_string(first, last, options, true) : +#endif + 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, @@ -347,13 +371,18 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, return answer; } #endif - parsed_number_string_t const pns = #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN - (chars_format_t(options.format & detail::basic_json_fmt)) - ? parse_number_string(first, last, options) - : + bool const bjf = chars_format_t(options.format & detail::basic_json_fmt) != 0; #endif - parse_number_string(first, last, options); + // 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 = +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + bjf ? parse_number_string(first, last, options, false) : +#endif + parse_number_string(first, last, options, false); if (pns.invalid) { #ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN if (chars_format_t(options.format & chars_format::no_infnan)) { @@ -368,8 +397,67 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value, #endif } - // 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. + // +// 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 +#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 +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + , + bjf +#endif + ); + } + answer.ec = std::errc(); // be optimistic + answer.ptr = pns.lastmatch; + + if (clinger_fast_path_impl(pns.mantissa, pns.exponent, +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + pns.negative, +#endif + 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 fastfloat_unlikely (am.power2 < 0) { + return parse_number_slow_path(first, last, value, options +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + , + bjf +#endif + ); + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + to_float( +#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN + pns.negative, +#endif + 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 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.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; } 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) diff --git a/tests/fast_int.cpp b/tests/fast_int.cpp index 94e76fd..10e7dcf 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 @@ -821,6 +824,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", @@ -1295,6 +1353,330 @@ 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; + } + } + + // 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; } 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 +} 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; 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; }