mirror of
https://github.com/nlohmann/json.git
synced 2026-10-23 06:18:56 +00:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1235cecc9 | ||
|
|
e28506dd86 | ||
|
|
03a0595c1e | ||
|
|
4e72af9d54 | ||
|
|
0f5d07310c | ||
|
|
0b924f5375 | ||
|
|
6fa14d8f95 | ||
|
|
dec0e9b749 | ||
|
|
86d5c56c54 | ||
|
|
aa0b371f7c | ||
|
|
ace1030107 | ||
|
|
2dc1e54856 | ||
|
|
9855db81f3 | ||
|
|
533cbfee51 | ||
|
|
9bf531436f | ||
|
|
3e031b06a5 | ||
|
|
7c26e99ac5 | ||
|
|
6aefd8c662 | ||
|
|
52fecc1fab | ||
|
|
fa37b952df | ||
|
|
e9970066c5 | ||
|
|
3f8677d028 | ||
|
|
ef3d350639 | ||
|
|
5e13f834c1 | ||
|
|
e13e976801 | ||
|
|
898e6ee81d | ||
|
|
ef1cdb59ac |
No files matched your search
@@ -29,6 +29,13 @@ updates:
|
||||
- dependency-name: mermaid
|
||||
update-types: ["version-update:semver-major"]
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: /.github/requirements
|
||||
schedule:
|
||||
interval: daily
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: pip
|
||||
directory: /tools/astyle
|
||||
schedule:
|
||||
@@ -36,7 +43,7 @@ updates:
|
||||
cooldown:
|
||||
default-days: 7
|
||||
ignore:
|
||||
# astyle is deliberately pinned (see tools/astyle/requirements.txt): newer versions
|
||||
# astyle is deliberately pinned (see tools/astyle/requirements.in): newer versions
|
||||
# reformat the code, and the pinned version defines the formatting CI enforces
|
||||
- dependency-name: astyle
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ environment:
|
||||
matrix:
|
||||
# The Visual Studio 2017 jobs compile everything with /std:c++17, so they
|
||||
# only build the C++17 variant of each test, split into two jobs each to
|
||||
# stay below AppVeyor's 60-minute limit per job.
|
||||
# stay below AppVeyor's 60-minute limit per job. The Visual Studio 2019
|
||||
# job builds the tests for all standards and is split for the same reason.
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
|
||||
configuration: Debug
|
||||
platform: x86
|
||||
@@ -50,7 +51,13 @@ environment:
|
||||
configuration: Release
|
||||
platform: x86
|
||||
CXX_FLAGS: "/W4 /WX"
|
||||
CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF"
|
||||
CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF -DJSON_TestShard=0/2"
|
||||
GENERATOR: Visual Studio 16 2019
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
configuration: Release
|
||||
platform: x86
|
||||
CXX_FLAGS: "/W4 /WX"
|
||||
CMAKE_OPTIONS: "-DJSON_ImplicitConversions=OFF -DJSON_TestShard=1/2"
|
||||
GENERATOR: Visual Studio 16 2019
|
||||
|
||||
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
|
||||
|
||||
@@ -64,6 +64,7 @@ labels:
|
||||
- "tests/src/unit-json_view.*"
|
||||
- "tests/src/json_view_test_helpers\\.hpp"
|
||||
- "tests/src/fuzzer-(parse_json_view|json_view_image)\\.cpp"
|
||||
- "tests/benchmarks/json_view/.*"
|
||||
- "tools/amalgamate/config_json_view\\.json"
|
||||
- "docs/mkdocs/docs/features/json_view\\.md"
|
||||
- "docs/mkdocs/docs/api/basic_json_(document|view)/.*"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
meson==1.12.1
|
||||
@@ -0,0 +1,10 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.14
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --no-emit-index-url --output-file=.github/requirements/meson.txt --strip-extras .github/requirements/meson.in
|
||||
#
|
||||
meson==1.12.1 \
|
||||
--hash=sha256:930bc7542cbd9f57009e182fd014eba48cf1a0180a7b9006d2d32d8f168d8b02 \
|
||||
--hash=sha256:ab0a6ca09f8ef70c564c8241fb5a23957886a0b53fb58412b5e07eaf07dba743
|
||||
# via -r .github/requirements/meson.in
|
||||
@@ -0,0 +1 @@
|
||||
semgrep==1.168.0
|
||||
File diff suppressed because it is too large.
Load diff
@@ -67,10 +67,10 @@ jobs:
|
||||
- name: Install astyle
|
||||
run: |
|
||||
python3 -mvenv venv
|
||||
venv/bin/pip3 install -r $MAIN_DIR/tools/astyle/requirements.txt
|
||||
venv/bin/pip3 install --require-hashes -r $MAIN_DIR/tools/astyle/requirements.txt
|
||||
|
||||
- name: Install generate_natvis dependencies
|
||||
run: pip3 install -r $NATVIS_TOOL_DIR/requirements.txt
|
||||
run: pip3 install --require-hashes -r $MAIN_DIR/tools/generate_natvis/requirements.txt
|
||||
|
||||
- name: Regenerate the tools/macro_builder tables in macro_scope.hpp
|
||||
run: |
|
||||
@@ -154,6 +154,28 @@ jobs:
|
||||
# PR content in a job without credentials.
|
||||
python3 tools/generate_natvis/generate_natvis.py $MAIN_DIR
|
||||
|
||||
- name: Check the nlohmann_json.natvis generator options
|
||||
run: |
|
||||
cd $MAIN_DIR
|
||||
# The options for configurations outside the checked-in file write
|
||||
# to a temporary file, so they do not affect the diff below.
|
||||
GENERATOR=tools/generate_natvis/generate_natvis.py
|
||||
python3 $GENERATOR --list-tags
|
||||
TAG_COUNT=$(python3 $GENERATOR --list-tags | wc -l)
|
||||
|
||||
python3 $GENERATOR --abi-tags diag,_dp,_vns --abi-tags _diag --output $RUNNER_TEMP/abi_tags.natvis
|
||||
grep -q '<!-- Namespace nlohmann::json_abi_diag_dp_vns -->' $RUNNER_TEMP/abi_tags.natvis
|
||||
grep -q '<!-- Namespace nlohmann::json_abi_diag_dp_vns_v[0-9_]* -->' $RUNNER_TEMP/abi_tags.natvis
|
||||
test $(grep -c '<!-- Namespace ' $RUNNER_TEMP/abi_tags.natvis) -eq $(( $(grep -c '<!-- Namespace ' nlohmann_json.natvis) + 2 ))
|
||||
|
||||
python3 $GENERATOR --all --output $RUNNER_TEMP/all.natvis
|
||||
test $(grep -c '<!-- Namespace ' $RUNNER_TEMP/all.natvis) -eq $(( (1 << TAG_COUNT) * 2 + 1 ))
|
||||
|
||||
if python3 $GENERATOR --abi-tags _unknown --output $RUNNER_TEMP/unknown.natvis; then
|
||||
echo "generate_natvis.py accepted an unknown ABI tag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build patch and check for differences
|
||||
id: diff
|
||||
run: |
|
||||
|
||||
@@ -38,14 +38,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
with:
|
||||
languages: c-cpp
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/autobuild@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
@@ -37,6 +37,14 @@ jobs:
|
||||
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "pr"
|
||||
})[0];
|
||||
// a run that failed before its jobs started (e.g. a stale queued
|
||||
// run cancelled long after its push) has no artifact to report on
|
||||
if (!matchArtifact) {
|
||||
core.notice('Run ${{ github.event.workflow_run.id }} has no "pr" artifact; nothing to comment on.');
|
||||
core.setOutput('found', 'false');
|
||||
return;
|
||||
}
|
||||
core.setOutput('found', 'true');
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
@@ -51,9 +59,11 @@ jobs:
|
||||
# Extract the untrusted PR artifact into a dedicated empty directory and
|
||||
# read only the two expected files by fixed path afterwards. This avoids a
|
||||
# malicious archive overwriting workspace files or escaping via ../ paths.
|
||||
- run: unzip -o pr.zip -d ./pr_artifact
|
||||
- if: steps.download.outputs.found == 'true'
|
||||
run: unzip -o pr.zip -d ./pr_artifact
|
||||
|
||||
- name: 'Comment on PR'
|
||||
if: steps.download.outputs.found == 'true'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
output: 'flawfinder_results.sarif'
|
||||
|
||||
- name: Upload analysis results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
with:
|
||||
sarif_file: ${{github.workspace}}/flawfinder_results.sarif
|
||||
@@ -0,0 +1,84 @@
|
||||
name: "json_view benchmarks"
|
||||
|
||||
# On demand only: runs the comparison of json_view with yyjson, simdjson, and
|
||||
# Boost.JSON (tests/benchmarks/json_view/compare.py) on GitHub-hosted runners,
|
||||
# for numbers from x86-64 and AArch64 Linux. It runs when started by hand, or
|
||||
# when a pull request gets the label "benchmark" (on both architectures, with
|
||||
# GCC and the default settings). Shared runners are noisy: the results show
|
||||
# where json_view stands, but published numbers need a quiet machine (see
|
||||
# tests/benchmarks/json_view/README.md).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
runner:
|
||||
description: "Runner image"
|
||||
type: choice
|
||||
options:
|
||||
- ubuntu-24.04
|
||||
- ubuntu-24.04-arm
|
||||
default: ubuntu-24.04
|
||||
compiler:
|
||||
description: "Compiler"
|
||||
type: choice
|
||||
options:
|
||||
- g++
|
||||
- clang++
|
||||
default: g++
|
||||
native:
|
||||
description: "Compile for the runner's CPU (-march=native)"
|
||||
type: boolean
|
||||
default: false
|
||||
rounds:
|
||||
description: "Rounds of bench_view"
|
||||
type: number
|
||||
default: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# (the label is part of the group: adding an unrelated label must not cancel a running benchmark)
|
||||
group: json-view-benchmarks-${{ github.workflow }}-${{ github.ref }}-${{ github.event.label.name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
compare:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'benchmark'
|
||||
strategy:
|
||||
matrix:
|
||||
runner: ${{ fromJSON(github.event_name == 'workflow_dispatch' && format('["{0}"]', inputs.runner) || '["ubuntu-24.04", "ubuntu-24.04-arm"]') }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download test data
|
||||
run: |
|
||||
cmake -S . -B build -DJSON_BuildTests=On
|
||||
cmake --build build --target download_test_data
|
||||
|
||||
- name: Run the comparison
|
||||
env:
|
||||
CXX: ${{ inputs.compiler || 'g++' }}
|
||||
CC: ${{ inputs.compiler == 'clang++' && 'clang' || 'gcc' }}
|
||||
ROUNDS: ${{ inputs.rounds || 30 }}
|
||||
NATIVE: ${{ inputs.native && '--native' || '' }}
|
||||
run: python3 tests/benchmarks/json_view/compare.py --data build/test_files --download --rounds "$ROUNDS" $NATIVE
|
||||
|
||||
- name: Summary
|
||||
run: cat tests/benchmarks/json_view/results/*.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: json_view-benchmarks-${{ matrix.runner }}-${{ inputs.compiler || 'g++' }}
|
||||
path: tests/benchmarks/json_view/results/
|
||||
@@ -80,6 +80,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
# SEMGREP_APP_TOKEN is still passed through so registry auth works if a
|
||||
# token is ever added.
|
||||
- name: Install Semgrep
|
||||
run: python3 -m pip install --user semgrep==1.168.0
|
||||
run: python3 -m pip install --user --require-hashes -r .github/requirements/semgrep.txt
|
||||
|
||||
# `semgrep scan --sarif` always exits 0 even with findings; continue-on-error
|
||||
# is a safety net so the SARIF upload still runs if the scan itself errors.
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
|
||||
# Upload SARIF file generated in previous step
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
||||
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
||||
with:
|
||||
sarif_file: semgrep.sarif
|
||||
if: always()
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Check that Meson and pkg-config offer the CMake options
|
||||
run: make check_build_options
|
||||
- name: Install Meson
|
||||
run: pip install meson
|
||||
run: pip install --require-hashes -r .github/requirements/meson.txt
|
||||
- name: Install with Meson
|
||||
run: |
|
||||
meson setup build-meson --prefix=${{ github.workspace }}/install
|
||||
|
||||
@@ -37,6 +37,7 @@ cc_library(
|
||||
"include/nlohmann/detail/input/parser.hpp",
|
||||
"include/nlohmann/detail/input/position_t.hpp",
|
||||
"include/nlohmann/detail/input/pow5_table.hpp",
|
||||
"include/nlohmann/detail/input/simd_scan.hpp",
|
||||
"include/nlohmann/detail/input/string_scan.hpp",
|
||||
"include/nlohmann/detail/iterators/internal_iterator.hpp",
|
||||
"include/nlohmann/detail/iterators/iter_impl.hpp",
|
||||
|
||||
@@ -6,6 +6,7 @@ This file describes the source for supporting files; that is, files that are not
|
||||
- [GitHub](#github)
|
||||
- [REUSE](#reuse)
|
||||
- [Package Managers](#package-managers)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
@@ -289,3 +290,29 @@ The module definition for [Bazel](https://bazel.build)'s [Bzlmod](https://bazel.
|
||||
Further documentation:
|
||||
|
||||
- [Bazel modules](https://bazel.build/external/module)
|
||||
|
||||
## Debugging
|
||||
|
||||
### `nlohmann_json.natvis`
|
||||
|
||||
The [Natvis](https://learn.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects) file that
|
||||
gives `json` values a friendly view in the Visual Studio debugger. The file is generated by
|
||||
`tools/generate_natvis/generate_natvis.py` from the template `tools/generate_natvis/nlohmann_json.natvis.j2` and the ABI
|
||||
tags and version in `include/nlohmann/detail/abi_macros.hpp`; change the script or the template rather than editing
|
||||
the file by hand. The file can be updated by calling
|
||||
|
||||
```shell
|
||||
make natvis
|
||||
```
|
||||
|
||||
The "Check amalgamation" workflow fails if the file is out of date.
|
||||
|
||||
The file covers the namespace variants with up to two ABI tags. A Natvis file for other configurations can be
|
||||
generated with the `--abi-tags` or `--all` options, for example
|
||||
`make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_nlohmann_json.natvis"`; see
|
||||
[`tools/generate_natvis`](tools/generate_natvis/README.md). With MSVC, CMake adds the file to the `nlohmann_json`
|
||||
target and installs it.
|
||||
|
||||
Further documentation:
|
||||
|
||||
- [Create custom views of C++ objects in the debugger using the Natvis framework](https://learn.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects)
|
||||
@@ -46,7 +46,7 @@ all:
|
||||
@echo "clean - remove built files"
|
||||
@echo "(fuzzers) - see tests/fuzzing.md, or run 'make -C tests fuzzers' to build the AFL fuzzers"
|
||||
@echo "macro_builder_check - check that macro_scope.hpp matches tools/macro_builder's output"
|
||||
@echo "natvis - regenerate nlohmann_json.natvis from the current ABI tags and version"
|
||||
@echo "natvis - regenerate nlohmann_json.natvis from the current ABI tags and version (pass generator options via NATVIS_ARGS, see tools/generate_natvis/README.md)"
|
||||
@echo "pretty - beautify code with Artistic Style"
|
||||
@echo "pvs_studio - run the PVS-Studio static analyzer and open the report"
|
||||
@echo "release - create the release files (include.zip, json.tar.xz, single headers, signatures, hashes)"
|
||||
@@ -120,13 +120,15 @@ $(AMALGAMATED_VIEW_FILE): $(SRCS)
|
||||
tools/amalgamate/amalgamate.py -c tools/amalgamate/config_json_view.json -s . --verbose=yes
|
||||
|
||||
NATVIS_VENV=tools/generate_natvis/venv
|
||||
# extra options for generate_natvis.py, e.g. NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my.natvis"
|
||||
NATVIS_ARGS=
|
||||
|
||||
# regenerate nlohmann_json.natvis from the ABI tags and version in include/nlohmann/detail/abi_macros.hpp
|
||||
# (the dependencies are installed into a virtual environment first)
|
||||
natvis:
|
||||
@test -d $(NATVIS_VENV) || python3 -mvenv $(NATVIS_VENV)
|
||||
@$(NATVIS_VENV)/bin/python3 -c 'import jinja2' 2>/dev/null || $(NATVIS_VENV)/bin/pip3 install --quiet -r tools/generate_natvis/requirements.txt
|
||||
$(NATVIS_VENV)/bin/python3 tools/generate_natvis/generate_natvis.py .
|
||||
$(NATVIS_VENV)/bin/python3 tools/generate_natvis/generate_natvis.py $(NATVIS_ARGS) .
|
||||
|
||||
# regenerate the two tools/macro_builder blocks of $(MACRO_SCOPE_HPP) (see its README.md) and diff against the
|
||||
# checked-in header; phony, because it never writes $(MACRO_SCOPE_HPP) itself
|
||||
|
||||
@@ -38,6 +38,7 @@ header. See also the [macro overview page](../../features/macros.md).
|
||||
- [**JSON_HAS_THREE_WAY_COMPARISON**](json_has_three_way_comparison.md) - control 3-way comparison support
|
||||
- [**JSON_NO_AUTOMATIC_UDLS**](json_no_automatic_udls.md) - do not include the user-defined string literals (UDLs) automatically
|
||||
- [**JSON_NO_IO**](json_no_io.md) - switch off functions relying on certain C++ I/O headers
|
||||
- [**JSON_NO_SIMD**](json_no_simd.md) - use only portable code instead of vector instructions (SSE2, SSSE3, NEON)
|
||||
- [**JSON_NO_THREAD_LOCAL**](json_no_thread_local.md) - switch off the use of `thread_local` storage
|
||||
- [**JSON_SKIP_UNSUPPORTED_COMPILER_CHECK**](json_skip_unsupported_compiler_check.md) - do not warn about unsupported compilers
|
||||
- [**JSON_USE_GLOBAL_UDLS**](json_use_global_udls.md) - place user-defined string literals (UDLs) into the global namespace
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# JSON_NO_SIMD
|
||||
|
||||
```cpp
|
||||
#define JSON_NO_SIMD
|
||||
```
|
||||
|
||||
When defined, the library uses only portable C++ code where it would otherwise use vector instructions: when scanning
|
||||
strings during parsing, and when writing floating-point numbers during serialization.
|
||||
|
||||
The macro is an escape hatch, for example for toolchains that cannot compile the vector code, for sanitizers or
|
||||
analyzers that do not understand it, or to compare the speed of both variants. It does not change any result: values,
|
||||
exceptions, and error positions are the same either way. Only the speed differs.
|
||||
|
||||
## Default definition
|
||||
|
||||
By default, `#!cpp JSON_NO_SIMD` is not defined, and vector instructions are used where the target has them.
|
||||
|
||||
```cpp
|
||||
#undef JSON_NO_SIMD
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
!!! note "Which instructions are used"
|
||||
|
||||
- **x86-64:** SSE2, which every x86-64 CPU has, finds the end of runs of plain string characters 16 bytes at a time
|
||||
and writes the digits of floating-point numbers. The UTF-8 check of non-ASCII text needs SSSE3, which is not part
|
||||
of x86-64: it is compiled for SSSE3 with a function attribute and used when the CPU has SSSE3 (every x86-64 CPU
|
||||
since about 2011), else the portable check. Code compiled for SSSE3 anyway (e.g., with `-mssse3` or `-march=native`)
|
||||
skips this check of the CPU. 32-bit x86 uses SSE2 for strings when the compiler targets it.
|
||||
- **AArch64:** NEON, which every 64-bit Arm CPU has, checks UTF-8 text and long runs of plain string characters 16
|
||||
bytes at a time, and writes the digits of floating-point numbers. With MSVC, only the floating-point digits use
|
||||
NEON.
|
||||
- **Other targets:** portable code only.
|
||||
|
||||
Strings are only scanned this way for contiguous input and for the characters a `#!cpp std::istream` holds in its
|
||||
stream buffer (see [`JSON_DISABLE_STREAM_BULK_SCAN`](json_disable_stream_bulk_scan.md)); other inputs are read one
|
||||
character at a time.
|
||||
|
||||
!!! note "Interaction with `JSON_USE_SIMDUTF`"
|
||||
|
||||
With [`JSON_USE_SIMDUTF`](json_use_simdutf.md), runs of string characters are first validated by simdutf; the code
|
||||
described here is used for what simdutf does not accept, so that errors are reported at the same position.
|
||||
`JSON_NO_SIMD` does not affect simdutf.
|
||||
|
||||
!!! note "ABI compatibility"
|
||||
|
||||
The macro selects between two definitions of the library's internal scanning and formatting functions. Whether it
|
||||
is defined is encoded in the [namespace](../../features/namespace.md) (tag `_ns`), resulting in distinct symbol
|
||||
names. Translation units compiled with and without it can therefore be linked into the same program without One
|
||||
Definition Rule (ODR) violations, but they cannot exchange instances of library types.
|
||||
|
||||
## Examples
|
||||
|
||||
??? example "Example: switching off vector instructions"
|
||||
|
||||
The program parses and serializes the same values with and without the macro:
|
||||
|
||||
```cpp
|
||||
#define JSON_NO_SIMD
|
||||
#include <iostream>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
int main()
|
||||
{
|
||||
const json j = json::parse(R"({"name": "日本語 text", "pi": 3.141592653589793})");
|
||||
std::cout << j.dump() << '\n'; // {"name":"日本語 text","pi":3.141592653589793}
|
||||
}
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [**parse**](../basic_json/parse.md) - deserialize from a compatible input
|
||||
- [**dump**](../basic_json/dump.md) - serialization
|
||||
- [**JSON_USE_SIMDUTF**](json_use_simdutf.md) - use the simdutf library to accelerate UTF-8 validation
|
||||
- [**JSON_DISABLE_STREAM_BULK_SCAN**](json_disable_stream_bulk_scan.md) - read a `std::istream` one character at a time
|
||||
- [Parsing: input sources and performance](../../features/parsing/index.md#input-sources-and-performance)
|
||||
|
||||
## Version history
|
||||
|
||||
- Added in version 3.13.0.
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
When defined, the parser validates the UTF-8 content of JSON strings that come from a **contiguous byte input**
|
||||
(`std::string`, `std::vector<char>`/`<std::uint8_t>`, string literals, `const char*` ranges, …) using the
|
||||
[simdutf](https://github.com/simdutf/simdutf) library instead of the built-in scalar validator. On text with many
|
||||
non-ASCII characters (e.g. CJK or emoji) this can validate several times faster.
|
||||
[simdutf](https://github.com/simdutf/simdutf) library instead of the built-in validator (which uses SSSE3 or NEON where
|
||||
available, see [`JSON_NO_SIMD`](json_no_simd.md)). On text with many non-ASCII characters (e.g. CJK or emoji) this can
|
||||
validate faster, depending on the CPU.
|
||||
|
||||
This is an **opt-in external dependency**. The library itself remains header-only and its behavior is unchanged: the
|
||||
same input is accepted or rejected either way, and every parse error is reported at the same position with the same
|
||||
|
||||
@@ -39,6 +39,6 @@ the library.
|
||||
## Version history
|
||||
|
||||
- Added in version 3.11.0. Changed inline namespace name in version 3.11.2. Added the ABI tag `_dp` in
|
||||
version 3.12.0, and the ABI tags `_bics`, `_psp`, `_snul`, `_sbu8`, `_ekmo`, `_dsbs`, `_dufio`, `_vns`, and
|
||||
`_vssse3` in version 3.13.0; see
|
||||
version 3.12.0, and the ABI tags `_bics`, `_psp`, `_snul`, `_sbu8`, `_ekmo`, `_dsbs`, `_dufio`, `_ns`, `_vns`,
|
||||
and `_vssse3` in version 3.13.0; see
|
||||
[`nlohmann` Namespace](../../features/namespace.md#structure).
|
||||
@@ -174,6 +174,13 @@ security reasons (e.g., Intel Software Guard Extensions (SGX)).
|
||||
|
||||
See [full documentation of `JSON_NO_IO`](../api/macros/json_no_io.md).
|
||||
|
||||
## `JSON_NO_SIMD`
|
||||
|
||||
When defined, the library uses only portable code instead of vector instructions (SSE2, SSSE3, NEON) for scanning
|
||||
strings and writing floating-point numbers. Results are the same either way. It is not defined by default.
|
||||
|
||||
See [full documentation of `JSON_NO_SIMD`](../api/macros/json_no_simd.md).
|
||||
|
||||
## `JSON_NO_THREAD_LOCAL`
|
||||
|
||||
When defined, the library does not use `#!cpp thread_local` storage. Copying a value and comparing two values then
|
||||
|
||||
@@ -27,6 +27,7 @@ The complete default namespace name is derived as follows:
|
||||
`_dsbs`.
|
||||
- [`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) defined non-zero appends
|
||||
`_dufio`.
|
||||
- [`JSON_NO_SIMD`](../api/macros/json_no_simd.md) defined appends `_ns`.
|
||||
- [`JSON_VIEW_NO_SIMD`](../api/macros/json_view_no_simd.md) defined appends `_vns`.
|
||||
- [`JSON_VIEW_USE_SSSE3`](../api/macros/json_view_use_ssse3.md) defined appends `_vssse3`.
|
||||
- The inline namespace ends with the suffix `_v` followed by the 3 components of the version number separated by
|
||||
@@ -114,6 +115,7 @@ follows:
|
||||
([`JSON_STRICT_BINARY_UTF8`](../api/macros/json_strict_binary_utf8.md)), `_ekmo`
|
||||
([`JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS`](../api/macros/json_use_objects_for_enum_keyed_maps.md)), `_dsbs`
|
||||
([`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md)), `_dufio`
|
||||
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md)), `_vns`
|
||||
([`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md)), `_ns`
|
||||
([`JSON_NO_SIMD`](../api/macros/json_no_simd.md)), `_vns`
|
||||
([`JSON_VIEW_NO_SIMD`](../api/macros/json_view_no_simd.md)), and `_vssse3`
|
||||
([`JSON_VIEW_USE_SSSE3`](../api/macros/json_view_use_ssse3.md)) in version 3.13.0.
|
||||
@@ -79,6 +79,10 @@ follow the usual `std::streambuf` contract, and
|
||||
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../../api/macros/json_disable_unlocked_file_io.md) for programs in which another
|
||||
thread must use the same `#!cpp FILE` object while it is parsed.
|
||||
|
||||
Where the target has them, strings are scanned with vector instructions: SSE2 on x86-64 (and SSSE3 for UTF-8 text, when
|
||||
the CPU has it) and NEON on AArch64. [`JSON_NO_SIMD`](../../api/macros/json_no_simd.md) selects portable code instead,
|
||||
again without changing any result.
|
||||
|
||||
## SAX vs. DOM parsing
|
||||
|
||||
The library offers two parsing models:
|
||||
|
||||
@@ -20,32 +20,39 @@ below apply on every supported standard. This adapter exposes two optimizations
|
||||
|
||||
- it can reconstruct already-consumed input on demand for error messages, instead of copying every character as it is
|
||||
read, and
|
||||
- the lexer can scan ordinary string characters directly out of the buffer, several bytes at a time, rather than one
|
||||
character (and one function call) at a time.
|
||||
- the lexer can scan ordinary string characters, numbers, and indentation directly out of the buffer, several bytes at
|
||||
a time, rather than one character (and one function call) at a time.
|
||||
|
||||
A `#!cpp std::istream` (including `#!cpp std::ifstream`) or `#!cpp FILE*`, by contrast, is read through
|
||||
`input_stream_adapter` or `file_input_adapter`, which read one character (or one block, for binary formats) at a time
|
||||
and expose neither optimization -- the lexer falls back to the same byte-at-a-time path it uses for any
|
||||
non-contiguous, general-purpose iterator range. An iterator pair over non-contiguous but random-access storage (e.g.
|
||||
`#!cpp std::deque<char>::iterator`) gets the first optimization but not the second, since the byte-scanning fast path
|
||||
additionally requires contiguous storage.
|
||||
A `#!cpp std::istream` (including `#!cpp std::ifstream`) gets the same bulk scanning: `input_stream_adapter` scans the
|
||||
characters its stream buffer already holds (the *get area*) and asks the stream buffer for more only when they are used
|
||||
up. Nothing is read ahead, so the stream is left exactly where it would be after reading one character at a time, and
|
||||
[`operator>>`](../api/operator_gtgt.md) can still read several values from one stream. A stream buffer without a get
|
||||
area is read one character at a time. A `#!cpp FILE*` is still read one character at a time, but `file_input_adapter`
|
||||
locks the file once per parse instead of once per character. See
|
||||
[input sources and performance](parsing/index.md#input-sources-and-performance) for the full table. According to the
|
||||
description of [#5828](https://github.com/nlohmann/json/pull/5828), parsing from a stream is within about 10 to 20 % of
|
||||
parsing an equivalent `#!cpp std::string`.
|
||||
|
||||
Practically: if the JSON text is already in memory, or small enough to read into memory, prefer passing a
|
||||
`#!cpp std::string`, a `#!cpp std::vector<char>`, or a pointer range to `parse` over a `#!cpp std::istream`. For a
|
||||
file, that means reading it into a string first and then parsing the string, rather than passing a
|
||||
`#!cpp std::ifstream` directly to `parse` -- the latter never benefits from either optimization:
|
||||
Inputs that are still read one character at a time: iterator ranges that are not contiguous (e.g.
|
||||
`#!cpp std::list<char>` or `#!cpp std::istreambuf_iterator<char>`), wide strings, and user-defined adapters. An
|
||||
iterator pair over non-contiguous but random-access storage (e.g. `#!cpp std::deque<char>::iterator`) gets the first
|
||||
optimization but not the second, since the byte-scanning fast path requires contiguous storage.
|
||||
|
||||
Practically: a file can be passed directly to `parse`, which also avoids the memory for a copy of the file. Reading it
|
||||
into a string first is no longer necessary for speed:
|
||||
|
||||
```cpp
|
||||
// gets the contiguous fast paths
|
||||
// fast: the stream buffer's get area is scanned in bulk
|
||||
std::ifstream f("example.json");
|
||||
std::string contents((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
json j = json::parse(contents);
|
||||
|
||||
// does not: input_stream_adapter has no fast path
|
||||
std::ifstream f2("example.json");
|
||||
json j2 = json::parse(f2);
|
||||
json j = json::parse(f);
|
||||
```
|
||||
|
||||
Two macros switch the stream optimizations off without changing any result:
|
||||
[`JSON_DISABLE_STREAM_BULK_SCAN`](../api/macros/json_disable_stream_bulk_scan.md) for stream buffers that do not follow
|
||||
the usual `#!cpp std::streambuf` contract, and
|
||||
[`JSON_DISABLE_UNLOCKED_FILE_IO`](../api/macros/json_disable_unlocked_file_io.md) for programs in which another thread
|
||||
must use the same `#!cpp FILE` object while it is parsed.
|
||||
|
||||
For contiguous input with many non-ASCII characters, [`JSON_USE_SIMDUTF`](../api/macros/json_use_simdutf.md) can
|
||||
additionally speed up UTF-8 validation by using the [simdutf](https://github.com/simdutf/simdutf) library instead of
|
||||
the built-in scalar validator; streaming inputs (files, `#!cpp std::istream`, wide strings, user-defined adapters)
|
||||
|
||||
@@ -64,7 +64,7 @@ below are relative to `include/nlohmann`.
|
||||
|-----------|----------|
|
||||
| Value type enumeration | [`detail/value_t.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/value_t.hpp) |
|
||||
| Input adapters | [`detail/input/input_adapters.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/input_adapters.hpp) |
|
||||
| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp) |
|
||||
| Lexer | [`detail/input/lexer.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/lexer.hpp), [`detail/input/number_parse.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/number_parse.hpp), [`detail/input/string_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/string_scan.hpp), [`detail/input/simd_scan.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/simd_scan.hpp) |
|
||||
| Parser | [`detail/input/parser.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/parser.hpp) |
|
||||
| SAX interface and DOM builders | [`detail/input/json_sax.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/json_sax.hpp) |
|
||||
| Binary format readers | [`detail/input/binary_reader.hpp`](https://github.com/nlohmann/json/blob/develop/include/nlohmann/detail/input/binary_reader.hpp) |
|
||||
@@ -339,6 +339,11 @@ The `serializer` (used by [`dump`](../api/basic_json/dump.md) and [`operator<<`]
|
||||
- `output_stream_adapter` writes to a `std::ostream`.
|
||||
- `output_string_adapter` appends to a string.
|
||||
|
||||
The `binary_writer` does not call an adapter for every byte. The `to_*` functions that return a `std::vector` write
|
||||
into the vector through a sink (`output_vector_sink`) that is not virtual. The functions that take an output adapter
|
||||
write through a sink (`output_adapter_sink`) that collects the output in a buffer of one kibibyte and passes it to the
|
||||
adapter in blocks, so that the adapter is called (and a stream locked) once per block instead of once per value.
|
||||
|
||||
## Value conversion
|
||||
|
||||
Values are converted from and to other types with the `JSONSerializer` template parameter. The default,
|
||||
|
||||
@@ -10,6 +10,29 @@ at its root, a [Natvis](https://learn.microsoft.com/en-us/visualstudio/debugger/
|
||||
file that gives `json`/`ordered_json` values a friendly, key/value debugger view instead of showing raw internal
|
||||
fields, when debugging with the MSVC debug engine (`cppvsdbg`) in Visual Studio or VS Code.
|
||||
|
||||
The checked-in visualizers cover namespace variants with zero, one, or two ABI tags. Configurations with three or more
|
||||
ABI tags are not covered and may show raw internal fields instead. To generate a Natvis file for such a configuration:
|
||||
|
||||
1. Check out the release tag of the library version you use.
|
||||
2. List the ABI tags and the macros that enable them:
|
||||
|
||||
```shell
|
||||
make natvis NATVIS_ARGS="--list-tags"
|
||||
```
|
||||
|
||||
3. Generate a Natvis file with the tags of your configuration, for example for `JSON_DIAGNOSTICS`,
|
||||
`JSON_DIAGNOSTIC_POSITIONS`, and `JSON_VIEW_NO_SIMD`:
|
||||
|
||||
```shell
|
||||
make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_nlohmann_json.natvis"
|
||||
```
|
||||
|
||||
4. Use the generated file instead of `nlohmann_json.natvis`.
|
||||
|
||||
`--abi-tags` can be repeated for several configurations, and `--all` generates every combination (about 60 MB for 12
|
||||
ABI tags, doubling with each further tag). See
|
||||
[`tools/generate_natvis`](https://github.com/nlohmann/json/tree/develop/tools/generate_natvis) for all options.
|
||||
|
||||
Debug engines that wrap LLDB instead of the MSVC debug engine (for example, `codelldb` in VS Code) only have
|
||||
partial/experimental Natvis support, and commonly fall back to showing raw internal fields even with the
|
||||
`.natvis` file present. Switching to `cppvsdbg` where available, or checking your debug extension's own Natvis
|
||||
|
||||
@@ -383,6 +383,7 @@ nav:
|
||||
- 'JSON_NO_AUTOMATIC_UDLS': api/macros/json_no_automatic_udls.md
|
||||
- 'JSON_NOEXCEPTION': api/macros/json_noexception.md
|
||||
- 'JSON_NO_IO': api/macros/json_no_io.md
|
||||
- 'JSON_NO_SIMD': api/macros/json_no_simd.md
|
||||
- 'JSON_NO_THREAD_LOCAL': api/macros/json_no_thread_local.md
|
||||
- 'JSON_PRECISE_STREAM_POSITION': api/macros/json_precise_stream_position.md
|
||||
- 'JSON_SKIP_LIBRARY_VERSION_CHECK': api/macros/json_skip_library_version_check.md
|
||||
|
||||
+24
-23
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "check-mermaid",
|
||||
"dependencies": {
|
||||
"jsdom": "30.1.2",
|
||||
"jsdom": "30.1.3",
|
||||
"mermaid": "11.17.2"
|
||||
}
|
||||
},
|
||||
@@ -40,9 +40,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.4.tgz",
|
||||
"integrity": "sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==",
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.3.tgz",
|
||||
"integrity": "sha512-3+777rEPsOOjoy5/sRmIzHnoRDaLpm3XWY2dmWn/N7f/XwDG0bcpnFaKL3X9FYJUbNwLmpcJC1bUYexJRxsFlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.1.0",
|
||||
@@ -1105,9 +1105,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.16",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz",
|
||||
"integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==",
|
||||
"version": "3.4.17",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
|
||||
"integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
@@ -1202,13 +1202,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.2.tgz",
|
||||
"integrity": "sha512-0FFE/jE1rppmVfUrJUgxqXjcwolZYIGtAgj1pTussMhNZxIxi7W/PvfWaMNroGi2B36X1IKHbexpZ9DhkoOPiQ==",
|
||||
"version": "30.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.3.tgz",
|
||||
"integrity": "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^7.1.3",
|
||||
"@asamuzakjp/dom-selector": "^9.2.4",
|
||||
"@asamuzakjp/dom-selector": "^10.0.3",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.15",
|
||||
"@exodus/bytes": "^1.16.0",
|
||||
@@ -1221,7 +1221,7 @@
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.11.2",
|
||||
"undici": "^8.12.1",
|
||||
"w3c-xmlserializer": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
@@ -1241,28 +1241,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.47",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
|
||||
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
|
||||
"version": "0.18.11",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.18.11.tgz",
|
||||
"integrity": "sha512-yvyz/2VMKqoho/35Hat5w37wIEdWJ5ElVMidTAlWrxr5uW4aBGsEpveTmKh18/zw08v/7lGswCPon4OhqmqrTA==",
|
||||
"deprecated": "Accidentally published with breaking changes. Use 0.19.0 instead.",
|
||||
"funding": [
|
||||
"https://opencollective.com/katex",
|
||||
"https://github.com/sponsors/katex"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^8.3.0"
|
||||
"commander": "^15.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/katex/node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
|
||||
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/khroma": {
|
||||
@@ -1521,9 +1522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz",
|
||||
"integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==",
|
||||
"version": "8.12.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
|
||||
"integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"description": "Validate the Mermaid diagrams of the documentation (see check_mermaid.mjs)",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"jsdom": "30.1.2",
|
||||
"jsdom": "30.1.3",
|
||||
"mermaid": "11.17.2"
|
||||
},
|
||||
"overrides": {
|
||||
"katex": "0.18.11"
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,12 @@
|
||||
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD _ns
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns
|
||||
#else
|
||||
@@ -139,9 +145,9 @@
|
||||
#endif
|
||||
|
||||
// Construct the namespace ABI tags component
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l)
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m)
|
||||
|
||||
#define NLOHMANN_JSON_ABI_TAGS \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||
@@ -155,6 +161,7 @@
|
||||
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \
|
||||
NLOHMANN_JSON_ABI_TAG_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3)
|
||||
|
||||
|
||||
@@ -14,11 +14,24 @@
|
||||
#include <intrin0.h> // __umulh, _umul128, _BitScanForward64, _BitScanReverse64
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
||||
#endif
|
||||
#if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || defined(_M_ARM))
|
||||
#include <intrin.h> // _BitScanForward, _BitScanReverse
|
||||
#endif
|
||||
|
||||
#include <nlohmann/detail/macro_scope.hpp> // JSON_HEDLEY_ALWAYS_INLINE, NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
|
||||
#if defined(__cpp_lib_bit_cast) || defined(__cpp_lib_bitops) || defined(__cpp_lib_byteswap)
|
||||
#include <bit> // bit_cast, byteswap, countl_zero, countr_zero
|
||||
#endif
|
||||
|
||||
// Portable bit-level helpers for the number and string scanners. They use
|
||||
// compiler builtins or platform-specific intrinsics where available and plain
|
||||
// C++ otherwise, so they work regardless of byte order.
|
||||
// compiler builtins or platform-specific intrinsics where available, the
|
||||
// C++20 <bit> functions next, and plain C++ otherwise, so they work regardless
|
||||
// of byte order. (The builtins and intrinsics come first: MSVC's
|
||||
// std::countl_zero checks for LZCNT at run time unless AVX2 is enabled.)
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -33,6 +46,16 @@ inline int count_leading_zeros(std::uint64_t x) noexcept
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
|
||||
_BitScanReverse64(&index, x);
|
||||
return 63 - static_cast<int>(index);
|
||||
#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM))
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes
|
||||
if (_BitScanReverse(&index, static_cast<unsigned long>(x >> 32u))) // NOLINT(runtime/int)
|
||||
{
|
||||
return 31 - static_cast<int>(index);
|
||||
}
|
||||
_BitScanReverse(&index, static_cast<unsigned long>(x)); // NOLINT(runtime/int)
|
||||
return 63 - static_cast<int>(index);
|
||||
#elif defined(__cpp_lib_bitops)
|
||||
return std::countl_zero(x);
|
||||
#else
|
||||
int n = 0;
|
||||
for (int shift = 32; shift != 0; shift >>= 1)
|
||||
@@ -56,6 +79,16 @@ inline int count_trailing_zeros(std::uint64_t x) noexcept
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan*64 takes
|
||||
_BitScanForward64(&index, x);
|
||||
return static_cast<int>(index);
|
||||
#elif defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_ARM))
|
||||
unsigned long index = 0; // NOLINT(runtime/int): the type _BitScan* takes
|
||||
if (_BitScanForward(&index, static_cast<unsigned long>(x))) // NOLINT(runtime/int)
|
||||
{
|
||||
return static_cast<int>(index);
|
||||
}
|
||||
_BitScanForward(&index, static_cast<unsigned long>(x >> 32u)); // NOLINT(runtime/int)
|
||||
return 32 + static_cast<int>(index);
|
||||
#elif defined(__cpp_lib_bitops)
|
||||
return std::countr_zero(x);
|
||||
#else
|
||||
int n = 0;
|
||||
for (int shift = 32; shift != 0; shift >>= 1)
|
||||
@@ -70,6 +103,67 @@ inline int count_trailing_zeros(std::uint64_t x) noexcept
|
||||
#endif
|
||||
}
|
||||
|
||||
/// the object representation of @a from as a @a To (std::bit_cast in C++20)
|
||||
template<typename To, typename From>
|
||||
JSON_HEDLEY_ALWAYS_INLINE To bit_cast(const From& from) noexcept
|
||||
{
|
||||
static_assert(sizeof(To) == sizeof(From), "bit_cast needs types of the same size");
|
||||
#if defined(__cpp_lib_bit_cast)
|
||||
return std::bit_cast<To>(from);
|
||||
#else
|
||||
To to {};
|
||||
std::memcpy(&to, &from, sizeof(To));
|
||||
return to;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// x with its bytes in reverse order (std::byteswap in C++23); compilers lower
|
||||
/// it to a single bswap/rev/movbe
|
||||
inline std::uint16_t byteswap(std::uint16_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap16(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ushort(x);
|
||||
#elif defined(__cpp_lib_byteswap)
|
||||
return std::byteswap(x);
|
||||
#else
|
||||
return static_cast<std::uint16_t>((x >> 8u) | (x << 8u));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// x with its bytes in reverse order (std::byteswap in C++23)
|
||||
inline std::uint32_t byteswap(std::uint32_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap32(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ulong(x);
|
||||
#elif defined(__cpp_lib_byteswap)
|
||||
return std::byteswap(x);
|
||||
#else
|
||||
return ((x & 0x000000FFu) << 24u) | ((x & 0x0000FF00u) << 8u)
|
||||
| ((x & 0x00FF0000u) >> 8u) | ((x & 0xFF000000u) >> 24u);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// x with its bytes in reverse order (std::byteswap in C++23)
|
||||
inline std::uint64_t byteswap(std::uint64_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap64(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_uint64(x);
|
||||
#elif defined(__cpp_lib_byteswap)
|
||||
return std::byteswap(x);
|
||||
#else
|
||||
x = ((x & 0x00000000FFFFFFFFull) << 32u) | ((x & 0xFFFFFFFF00000000ull) >> 32u);
|
||||
x = ((x & 0x0000FFFF0000FFFFull) << 16u) | ((x & 0xFFFF0000FFFF0000ull) >> 16u);
|
||||
x = ((x & 0x00FF00FF00FF00FFull) << 8u) | ((x & 0xFF00FF00FF00FF00ull) >> 8u);
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// the 128-bit product of two 64-bit numbers
|
||||
struct uint128_parts
|
||||
{
|
||||
@@ -107,8 +201,8 @@ inline uint128_parts full_multiplication(std::uint64_t a, std::uint64_t b) noexc
|
||||
/// targets; always inlined, as GCC otherwise calls it in the number loops)
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::uint64_t read_eight_bytes(const unsigned char* b) noexcept
|
||||
{
|
||||
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) || (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
||||
// the byte order already matches (all MSVC targets are little-endian)
|
||||
#if JSON_LITTLE_ENDIAN_TARGET
|
||||
// the byte order already matches
|
||||
std::uint64_t result = 0;
|
||||
std::memcpy(&result, b, sizeof(result));
|
||||
return result;
|
||||
|
||||
@@ -18,17 +18,14 @@
|
||||
#include <limits> // numeric_limits
|
||||
#include <type_traits> // conditional
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <cstdlib> // _byteswap_uint64
|
||||
#endif
|
||||
|
||||
// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16
|
||||
// digits of a double at once
|
||||
#if defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC))
|
||||
// digits of a double at once (unless JSON_NO_SIMD selects the portable code)
|
||||
#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)))
|
||||
#include <emmintrin.h>
|
||||
#define JSON_DTOA_SSE2 1
|
||||
#define JSON_DTOA_NEON 0
|
||||
#elif (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN)
|
||||
#elif !defined(JSON_NO_SIMD) && (defined(__aarch64__) || defined(_M_ARM64)) && !defined(_M_ARM64EC) && !defined(__ARM_BIG_ENDIAN)
|
||||
#include <arm_neon.h>
|
||||
#define JSON_DTOA_SSE2 0
|
||||
#define JSON_DTOA_NEON 1
|
||||
@@ -37,6 +34,7 @@
|
||||
#define JSON_DTOA_NEON 0
|
||||
#endif
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/conversions/zmij.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
@@ -66,16 +64,6 @@ For a detailed description of the algorithm see:
|
||||
namespace dtoa_impl
|
||||
{
|
||||
|
||||
template<typename Target, typename Source>
|
||||
Target reinterpret_bits(const Source source)
|
||||
{
|
||||
static_assert(sizeof(Target) == sizeof(Source), "size mismatch");
|
||||
|
||||
Target target;
|
||||
std::memcpy(&target, &source, sizeof(Source));
|
||||
return target;
|
||||
}
|
||||
|
||||
struct diyfp // f * 2^e
|
||||
{
|
||||
static constexpr int kPrecision = 64; // = q
|
||||
@@ -230,7 +218,7 @@ boundaries compute_boundaries(FloatType value)
|
||||
|
||||
using bits_type = typename std::conditional<kPrecision == 24, std::uint32_t, std::uint64_t >::type;
|
||||
|
||||
const auto bits = static_cast<std::uint64_t>(reinterpret_bits<bits_type>(value));
|
||||
const auto bits = static_cast<std::uint64_t>(bit_cast<bits_type>(value));
|
||||
const std::uint64_t E = bits >> (kPrecision - 1);
|
||||
const std::uint64_t F = bits & (kHiddenBit - 1);
|
||||
|
||||
@@ -1084,13 +1072,10 @@ inline std::uint64_t eight_digit_bytes(std::uint64_t abcdefgh) noexcept
|
||||
/// the byte stores once this is inlined)
|
||||
inline void store_msb_first(char* p, std::uint64_t v) noexcept
|
||||
{
|
||||
#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
v = __builtin_bswap64(v);
|
||||
#if JSON_LITTLE_ENDIAN_TARGET
|
||||
v = byteswap(v);
|
||||
std::memcpy(p, &v, sizeof(v));
|
||||
#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
std::memcpy(p, &v, sizeof(v));
|
||||
#elif defined(_MSC_VER) // (little-endian on all its targets)
|
||||
v = _byteswap_uint64(v);
|
||||
#elif JSON_BIG_ENDIAN_TARGET
|
||||
std::memcpy(p, &v, sizeof(v));
|
||||
#else
|
||||
for (unsigned i = 0; i < 8; ++i)
|
||||
@@ -1462,8 +1447,7 @@ char* write_positive_zmij(char* first, const char* last, FloatType value)
|
||||
{
|
||||
static_assert(is_binary64<FloatType>::value,
|
||||
"internal error: the conversion of Zmij needs IEEE 754 binary64 numbers");
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
const auto bits = bit_cast<std::uint64_t>(value);
|
||||
const zmij::shortest_decimal d = zmij::to_shortest(bits);
|
||||
if (JSON_HEDLEY_LIKELY(last - first >= 41))
|
||||
{
|
||||
|
||||
@@ -8,17 +8,18 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm> // count, find, min
|
||||
#include <algorithm> // count, find, max, min
|
||||
#include <array> // array
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint32_t
|
||||
#include <cstring> // strlen
|
||||
#include <iterator> // begin, end, iterator_traits, random_access_iterator_tag, distance, next, reverse_iterator
|
||||
#include <limits> // numeric_limits
|
||||
#include <memory> // unique_ptr
|
||||
#include <streambuf> // streambuf
|
||||
#include <string> // string, char_traits
|
||||
#include <type_traits> // enable_if, is_base_of, is_pointer, is_integral, remove_pointer
|
||||
#include <utility> // pair, declval
|
||||
#include <utility> // declval, move, pair
|
||||
#include <vector> // vector
|
||||
|
||||
#ifndef JSON_NO_IO
|
||||
@@ -72,6 +73,82 @@ enum class input_format_t { json, cbor, msgpack, ubjson, bson, bjdata, bon8 };
|
||||
|
||||
#ifndef JSON_NO_IO
|
||||
/*!
|
||||
A growable array of characters for the part of a token that an input adapter
|
||||
copied out of its window: unlike std::vector<char>::push_back(), appending a
|
||||
character takes no reference to a local, and is a few instructions when there
|
||||
is room.
|
||||
*/
|
||||
class token_carry_buffer
|
||||
{
|
||||
public:
|
||||
token_carry_buffer() = default;
|
||||
token_carry_buffer(const token_carry_buffer&) = delete;
|
||||
token_carry_buffer& operator=(const token_carry_buffer&) = delete;
|
||||
token_carry_buffer(token_carry_buffer&& rhs) noexcept
|
||||
: m_data(std::move(rhs.m_data)), m_end(rhs.m_end), m_capacity_end(rhs.m_capacity_end)
|
||||
{
|
||||
rhs.m_end = rhs.m_capacity_end = nullptr;
|
||||
}
|
||||
token_carry_buffer& operator=(token_carry_buffer&&) = delete;
|
||||
~token_carry_buffer() = default;
|
||||
|
||||
void clear() noexcept
|
||||
{
|
||||
m_end = m_data.get();
|
||||
}
|
||||
|
||||
void push_back(char c)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(m_end == m_capacity_end))
|
||||
{
|
||||
grow(1);
|
||||
}
|
||||
*m_end++ = c;
|
||||
}
|
||||
|
||||
void append(const char* first, const char* last)
|
||||
{
|
||||
const auto n = static_cast<std::size_t>(last - first);
|
||||
const auto room = static_cast<std::size_t>(m_capacity_end - m_end);
|
||||
if (n > room)
|
||||
{
|
||||
grow(n);
|
||||
}
|
||||
if (n != 0)
|
||||
{
|
||||
std::memcpy(m_end, first, n);
|
||||
}
|
||||
m_end += n;
|
||||
}
|
||||
|
||||
/// replace the contents of @a out
|
||||
void copy_to(std::vector<char>& out) const
|
||||
{
|
||||
out.assign(m_data.get(), m_end);
|
||||
}
|
||||
|
||||
private:
|
||||
/// make room for @a extra more characters
|
||||
JSON_HEDLEY_NEVER_INLINE void grow(std::size_t extra)
|
||||
{
|
||||
const auto size = static_cast<std::size_t>(m_end - m_data.get());
|
||||
const auto capacity = static_cast<std::size_t>(m_capacity_end - m_data.get());
|
||||
const std::size_t new_capacity = (std::max)(capacity < 32 ? static_cast<std::size_t>(32) : 2 * capacity, size + extra);
|
||||
std::unique_ptr<char[]> data(new char[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
if (size != 0)
|
||||
{
|
||||
std::memcpy(data.get(), m_data.get(), size);
|
||||
}
|
||||
m_data = std::move(data);
|
||||
m_end = m_data.get() + size;
|
||||
m_capacity_end = m_data.get() + new_capacity;
|
||||
}
|
||||
|
||||
std::unique_ptr<char[]> m_data{}; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays,readability-redundant-member-init)
|
||||
char* m_end = nullptr;
|
||||
char* m_capacity_end = nullptr;
|
||||
};
|
||||
/*!
|
||||
Input adapter for stdio file access. This adapter read only 1 byte and do not use any
|
||||
buffer. This adapter is a very low level adapter.
|
||||
*/
|
||||
@@ -209,6 +286,7 @@ class input_stream_adapter
|
||||
, token_segment(rhs.token_segment)
|
||||
, current_in_get_area(rhs.current_in_get_area)
|
||||
, tracking_token(rhs.tracking_token)
|
||||
, no_get_area(rhs.no_get_area)
|
||||
#endif
|
||||
{
|
||||
rhs.is = nullptr;
|
||||
@@ -295,37 +373,20 @@ class input_stream_adapter
|
||||
std::char_traits<char>::int_type get_character()
|
||||
{
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
// sbumpc() refills the get area if it is exhausted
|
||||
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
|
||||
if (JSON_HEDLEY_UNLIKELY(refill))
|
||||
{
|
||||
save_token_segment();
|
||||
}
|
||||
#endif
|
||||
|
||||
auto res = sb->sbumpc();
|
||||
// set eof manually, as we don't use the istream interface.
|
||||
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
|
||||
{
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
current_in_get_area = false;
|
||||
#endif
|
||||
is->clear(is->rdstate() | std::ios::eofbit);
|
||||
}
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
else if (JSON_HEDLEY_UNLIKELY(refill))
|
||||
{
|
||||
// the consumed character is at gptr() - 1, unless the streambuf
|
||||
// has no get area and returned it from uflow()
|
||||
const char* const next = streambuf_access::get_ptr(sb);
|
||||
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
|
||||
}
|
||||
else
|
||||
// the character is in the get area: step over it, as sbumpc() does
|
||||
const char* const next = streambuf_access::get_ptr(sb);
|
||||
if (JSON_HEDLEY_LIKELY(next != streambuf_access::get_end(sb)))
|
||||
{
|
||||
current_in_get_area = true;
|
||||
streambuf_access::bump(sb, 1);
|
||||
return std::char_traits<char>::to_int_type(*next);
|
||||
}
|
||||
if (no_get_area)
|
||||
{
|
||||
return get_character_no_get_area();
|
||||
}
|
||||
#endif
|
||||
return res;
|
||||
return get_character_slow();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -334,6 +395,18 @@ class input_stream_adapter
|
||||
{
|
||||
#if JSON_PRECISE_STREAM_POSITION
|
||||
commit_lookahead();
|
||||
#endif
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
// the elements are in the get area: copy them, as sgetn() does, but
|
||||
// without the virtual call
|
||||
const char* const first = streambuf_access::get_ptr(sb);
|
||||
const auto available = static_cast<std::size_t>(streambuf_access::get_end(sb) - first);
|
||||
if (JSON_HEDLEY_LIKELY(count * sizeof(T) <= available && count * sizeof(T) <= static_cast<std::size_t>((std::numeric_limits<int>::max)())))
|
||||
{
|
||||
std::memcpy(dest, first, count * sizeof(T));
|
||||
streambuf_access::bump(sb, static_cast<int>(count * sizeof(T)));
|
||||
return count * sizeof(T);
|
||||
}
|
||||
#endif
|
||||
auto res = static_cast<std::size_t>(sb->sgetn(reinterpret_cast<char*>(dest), static_cast<std::streamsize>(count * sizeof(T))));
|
||||
if (JSON_HEDLEY_UNLIKELY(res < count * sizeof(T)))
|
||||
@@ -421,7 +494,7 @@ class input_stream_adapter
|
||||
// last one if @a drop_last.
|
||||
void copy_token(std::vector<char>& out, bool drop_last) const
|
||||
{
|
||||
out = token_carry;
|
||||
token_carry.copy_to(out);
|
||||
if (token_segment != nullptr)
|
||||
{
|
||||
out.insert(out.end(), token_segment, current_end());
|
||||
@@ -434,6 +507,47 @@ class input_stream_adapter
|
||||
#endif
|
||||
|
||||
private:
|
||||
#if !JSON_PRECISE_STREAM_POSITION
|
||||
// the get area is exhausted, or there is none
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
#endif
|
||||
std::char_traits<char>::int_type get_character_slow()
|
||||
{
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
// sbumpc() refills the get area if it is exhausted
|
||||
const bool refill = streambuf_access::get_ptr(sb) == streambuf_access::get_end(sb);
|
||||
if (JSON_HEDLEY_UNLIKELY(refill))
|
||||
{
|
||||
save_token_segment();
|
||||
}
|
||||
#endif
|
||||
|
||||
auto res = sb->sbumpc();
|
||||
// set eof manually, as we don't use the istream interface.
|
||||
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof()))
|
||||
{
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
current_in_get_area = false;
|
||||
#endif
|
||||
is->clear(is->rdstate() | std::ios::eofbit);
|
||||
}
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
else if (JSON_HEDLEY_UNLIKELY(refill))
|
||||
{
|
||||
// the consumed character is at gptr() - 1, unless the streambuf
|
||||
// has no get area and returned it from uflow()
|
||||
const char* const next = streambuf_access::get_ptr(sb);
|
||||
after_refill(res, next != nullptr && next != streambuf_access::get_begin(sb));
|
||||
}
|
||||
else
|
||||
{
|
||||
current_in_get_area = true;
|
||||
}
|
||||
#endif
|
||||
return res;
|
||||
}
|
||||
#endif
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
// Reads the protected get-area pointers of any std::streambuf. Naming the
|
||||
// members through a derived class makes forming the member pointers
|
||||
@@ -457,6 +571,11 @@ class input_stream_adapter
|
||||
{
|
||||
(s->*(&streambuf_access::gbump))(n);
|
||||
}
|
||||
// sbumpc() for an exhausted get area
|
||||
static std::streambuf::int_type next_character(std::streambuf* s)
|
||||
{
|
||||
return (s->*(&streambuf_access::uflow))();
|
||||
}
|
||||
};
|
||||
|
||||
// one past the character last returned by get_character(), if that is in
|
||||
@@ -477,16 +596,53 @@ class input_stream_adapter
|
||||
{
|
||||
if (token_segment != nullptr)
|
||||
{
|
||||
token_carry.insert(token_carry.end(), token_segment, streambuf_access::get_end(sb));
|
||||
token_carry.append(token_segment, streambuf_access::get_end(sb));
|
||||
token_segment = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#if !JSON_PRECISE_STREAM_POSITION
|
||||
// The streambuf has no get area (e.g., std::cin synchronized with stdio):
|
||||
// every character comes from uflow(), and the token is kept by value.
|
||||
JSON_HEDLEY_NEVER_INLINE std::char_traits<char>::int_type get_character_no_get_area()
|
||||
{
|
||||
// the get area is exhausted, so this is what sbumpc() does
|
||||
const auto res = streambuf_access::next_character(sb);
|
||||
if (JSON_HEDLEY_UNLIKELY(res == std::char_traits<char>::eof() || streambuf_access::get_end(sb) != nullptr))
|
||||
{
|
||||
return get_character_no_get_area_other(res);
|
||||
}
|
||||
// current_in_get_area is false: only the first character of a get
|
||||
// area, or the end of the input, is read here
|
||||
if (tracking_token)
|
||||
{
|
||||
token_carry.push_back(std::char_traits<char>::to_char_type(res));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// @a res came from uflow() of a streambuf without a get area: it is the
|
||||
// end of the input, or the streambuf has a get area now
|
||||
JSON_HEDLEY_NEVER_INLINE std::char_traits<char>::int_type get_character_no_get_area_other(std::char_traits<char>::int_type res)
|
||||
{
|
||||
if (res == std::char_traits<char>::eof())
|
||||
{
|
||||
current_in_get_area = false;
|
||||
is->clear(is->rdstate() | std::ios::eofbit);
|
||||
return res;
|
||||
}
|
||||
// the consumed character is at gptr() - 1
|
||||
after_refill(res, streambuf_access::get_ptr(sb) != streambuf_access::get_begin(sb));
|
||||
return res;
|
||||
}
|
||||
#endif
|
||||
|
||||
// character @a c was just read into a new get area (@a in_get_area) or
|
||||
// returned without one
|
||||
void after_refill(std::char_traits<char>::int_type c, bool in_get_area)
|
||||
{
|
||||
current_in_get_area = in_get_area;
|
||||
no_get_area = !in_get_area;
|
||||
if (!tracking_token)
|
||||
{
|
||||
return;
|
||||
@@ -525,13 +681,16 @@ class input_stream_adapter
|
||||
#endif
|
||||
#if !JSON_DISABLE_STREAM_BULK_SCAN
|
||||
/// the part of the current token that was in earlier get areas
|
||||
std::vector<char> token_carry{}; // NOLINT(readability-redundant-member-init)
|
||||
token_carry_buffer token_carry{}; // NOLINT(readability-redundant-member-init)
|
||||
/// start of the current token's part in the get area, or nullptr
|
||||
const char* token_segment = nullptr;
|
||||
/// whether the character last returned by get_character() is in the get area
|
||||
bool current_in_get_area = false;
|
||||
/// whether a token is tracked at all; the binary readers never start one
|
||||
bool tracking_token = false;
|
||||
/// whether the streambuf had no get area when it was last asked for a
|
||||
/// character
|
||||
bool no_get_area = false;
|
||||
#endif
|
||||
};
|
||||
#endif // JSON_NO_IO
|
||||
|
||||
@@ -418,8 +418,9 @@ class json_sax_dom_parser
|
||||
JSON_ASSERT(!ref_stack.empty());
|
||||
JSON_ASSERT(ref_stack.back()->is_object());
|
||||
|
||||
// add null at the given key and store the reference for later
|
||||
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val));
|
||||
// add null at the given key and store the reference for later; the
|
||||
// key is moved, as the lexer starts the next token with reset()
|
||||
object_element = &(ref_stack.back()->m_data.m_value.object->operator[](std::move(val)));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint32_t
|
||||
#include <cstdio> // snprintf
|
||||
#include <cstring> // memcmp
|
||||
#include <initializer_list> // initializer_list
|
||||
#include <string> // char_traits, string
|
||||
#include <utility> // move
|
||||
@@ -417,30 +418,43 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
}
|
||||
|
||||
/// contiguous input: bulk-append the run of ordinary characters and complete
|
||||
/// well-formed UTF-8 sequences starting at the current read position, leaving
|
||||
/// the first byte that needs individual handling (the closing quote, an
|
||||
/// escape, a control character, or an ill-formed UTF-8 byte) for get()
|
||||
void scan_string_bulk(std::true_type /*bulk*/)
|
||||
/// well-formed UTF-8 sequences starting at the current read position, then
|
||||
/// read the first byte that needs individual handling (the closing quote, an
|
||||
/// escape, a control character, or an ill-formed UTF-8 byte) like get().
|
||||
/// That byte is taken from the buffer that was just scanned. Through get(),
|
||||
/// Apple Clang merged this read with the other get() calls of the scan
|
||||
/// function, which load the read position again right after it was stored
|
||||
/// (3% of json::accept() on jeopardy.json on Apple M1).
|
||||
char_int_type get_after_string_run(std::true_type /*bulk*/)
|
||||
{
|
||||
// a pending unget must be consumed through the normal path first
|
||||
if (next_unget)
|
||||
{
|
||||
return;
|
||||
return get();
|
||||
}
|
||||
const std::size_t remaining = ia.bulk_remaining();
|
||||
if (remaining == 0)
|
||||
{
|
||||
return;
|
||||
return get();
|
||||
}
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(ia.bulk_data());
|
||||
const auto* const raw = ia.bulk_data();
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(raw);
|
||||
|
||||
const std::size_t pos = string_bulk_run(data, remaining);
|
||||
if (pos == 0)
|
||||
const std::size_t pos = lexer_string_bulk_run(data, remaining);
|
||||
if (pos != 0)
|
||||
{
|
||||
return;
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
||||
bulk_consume(reinterpret_cast<const char*>(data), pos);
|
||||
}
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), pos);
|
||||
bulk_consume(reinterpret_cast<const char*>(data), pos);
|
||||
if (pos == remaining)
|
||||
{
|
||||
return get(); // end of the contiguous input
|
||||
}
|
||||
// what get() does, with the byte at hand
|
||||
advance_position();
|
||||
ia.bulk_skip(1);
|
||||
current = char_traits<char_type>::to_int_type(raw[pos]);
|
||||
return track_after_read();
|
||||
}
|
||||
|
||||
/// consume @a n characters at @a data (the adapter's bulk_data()) and do
|
||||
@@ -473,7 +487,10 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
}
|
||||
|
||||
/// streaming input: no bulk fast path
|
||||
void scan_string_bulk(std::false_type /*bulk*/) const noexcept {}
|
||||
char_int_type get_after_string_run(std::false_type /*bulk*/)
|
||||
{
|
||||
return get();
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief scan a string literal
|
||||
@@ -501,11 +518,8 @@ class lexer : public lexer_base<BasicJsonType>
|
||||
while (true)
|
||||
{
|
||||
// bulk-consume ordinary characters from contiguous input, then
|
||||
// handle the next special byte through the switch below
|
||||
scan_string_bulk(std::integral_constant<bool, bulk_scan> {});
|
||||
|
||||
// get the next character
|
||||
switch (get())
|
||||
// get the next character, the one that ends the run
|
||||
switch (get_after_string_run(std::integral_constant<bool, bulk_scan>()))
|
||||
{
|
||||
// end of file while parsing the string
|
||||
case char_traits<char_type>::eof():
|
||||
@@ -1625,9 +1639,12 @@ scan_number_done:
|
||||
const std::size_t avail = rem + 1;
|
||||
|
||||
// validate + classify the number extent (mirrors scan_number()'s grammar)
|
||||
// and accumulate the mantissa digits on the way, so that tokens with up
|
||||
// to 19 digits need no second pass for their value
|
||||
std::size_t i = 0;
|
||||
std::size_t dot_index = std::string::npos;
|
||||
token_type number_type = token_type::value_unsigned;
|
||||
std::uint64_t w = 0;
|
||||
if (data[0] == '-')
|
||||
{
|
||||
number_type = token_type::value_integer;
|
||||
@@ -1637,22 +1654,21 @@ scan_number_done:
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
}
|
||||
const std::size_t integer_begin = i;
|
||||
if (data[i] == '0')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
else if (data[i] >= '1' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
i = accumulate_digits(data, i, avail, w);
|
||||
}
|
||||
else
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
std::size_t digit_count = i - integer_begin;
|
||||
std::size_t fraction_digits = 0;
|
||||
if (i < avail && data[i] == '.')
|
||||
{
|
||||
number_type = token_type::value_float;
|
||||
@@ -1662,29 +1678,39 @@ scan_number_done:
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
++i;
|
||||
}
|
||||
const std::size_t fraction_begin = i;
|
||||
i = accumulate_digits(data, i, avail, w);
|
||||
fraction_digits = i - fraction_begin;
|
||||
digit_count += fraction_digits;
|
||||
}
|
||||
// the mantissa ends here, whether or not an exponent part follows
|
||||
const std::size_t mantissa_end = i;
|
||||
std::int64_t exponent = 0;
|
||||
if (i < avail && (data[i] == 'e' || data[i] == 'E'))
|
||||
{
|
||||
number_type = token_type::value_float;
|
||||
++i;
|
||||
bool negative_exponent = false;
|
||||
if (i < avail && (data[i] == '+' || data[i] == '-'))
|
||||
{
|
||||
negative_exponent = data[i] == '-';
|
||||
++i;
|
||||
}
|
||||
if (i >= avail || !(data[i] >= '0' && data[i] <= '9'))
|
||||
{
|
||||
return token_type::uninitialized;
|
||||
}
|
||||
// saturated far beyond every range, see parse_float_exponent()
|
||||
constexpr std::int64_t saturation = 100000000000000000; // 10^17
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
if (exponent < saturation)
|
||||
{
|
||||
exponent = (exponent * 10) + (data[i] - '0');
|
||||
}
|
||||
++i;
|
||||
}
|
||||
exponent = negative_exponent ? -exponent : exponent;
|
||||
}
|
||||
const std::size_t len = i;
|
||||
if (number_may_continue(len == avail, std::integral_constant<bool, bulk_window> {}))
|
||||
@@ -1705,7 +1731,10 @@ scan_number_done:
|
||||
#if !JSON_DIAGNOSTIC_POSITIONS
|
||||
if (number_type != token_type::value_float)
|
||||
{
|
||||
const token_type integer_result = convert_integer(number_type, data, data + len);
|
||||
// up to 18 digits always fit into std::uint64_t and std::int64_t
|
||||
const token_type integer_result = (digit_count <= 18)
|
||||
? store_integer(number_type, w)
|
||||
: convert_integer(number_type, data, data + len);
|
||||
if (JSON_HEDLEY_LIKELY(integer_result != token_type::uninitialized))
|
||||
{
|
||||
bulk_consume(data + 1, len - 1);
|
||||
@@ -1719,15 +1748,42 @@ scan_number_done:
|
||||
}
|
||||
#endif
|
||||
|
||||
// materialize the token exactly as scan_number() would. reset() already
|
||||
// cleared token_buffer, so append() fills it (assign() is avoided
|
||||
// because custom string_t types need not provide it)
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||
decimal_point_position = dot_index;
|
||||
|
||||
bulk_consume(data + 1, len - 1);
|
||||
read_number_terminator(std::integral_constant<bool, bulk_window> {});
|
||||
|
||||
// a float whose digits all fit into w converts from the accumulated
|
||||
// value; longer ones (and other float formats) take the general path
|
||||
if (number_type == token_type::value_float && digit_count <= 19)
|
||||
{
|
||||
float_significand s;
|
||||
s.w = w;
|
||||
s.exponent = exponent - static_cast<std::int64_t>(fraction_digits);
|
||||
s.negative = data[0] == '-';
|
||||
if (convert_accumulated_float(s, std::integral_constant<bool, has_native_float_format<number_float_t>::value> {}))
|
||||
{
|
||||
// the token text is only copied into token_buffer when
|
||||
// get_string() asks for it; the library's own SAX handlers
|
||||
// ignore it. A window into a stream may be refilled by the
|
||||
// next read, so there the text is copied right away.
|
||||
if (bulk_window)
|
||||
{
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||
}
|
||||
else
|
||||
{
|
||||
pending_number_text = data;
|
||||
pending_number_length = len;
|
||||
}
|
||||
return token_type::value_float;
|
||||
}
|
||||
}
|
||||
|
||||
// materialize the token exactly as scan_number() would. reset() already
|
||||
// cleared token_buffer, so append() fills it (assign() is avoided
|
||||
// because custom string_t types need not provide it)
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(data), len);
|
||||
return convert_number(number_type, mantissa_end);
|
||||
}
|
||||
|
||||
@@ -1756,6 +1812,35 @@ scan_number_done:
|
||||
/// the input is not handed back, so the terminator can stay unread
|
||||
void read_number_terminator(std::false_type /*window*/) const noexcept {}
|
||||
|
||||
/// binary32/binary64: the value of a float token from its accumulated
|
||||
/// significand (see scan_number_bulk_contiguous())
|
||||
bool convert_accumulated_float(const float_significand& s, std::true_type /*native*/) noexcept
|
||||
{
|
||||
value_float = decimal_to_float<number_float_t>(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// other formats (e.g. x87 long double): not instantiated for them, as
|
||||
/// decimal_to_float() only supports binary32/binary64
|
||||
static bool convert_accumulated_float(const float_significand& /*s*/, std::false_type /*native*/) noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// the value of an integer token with at most 18 digits, accumulated in @a w
|
||||
token_type store_integer(token_type number_type, std::uint64_t w) noexcept
|
||||
{
|
||||
if (number_type == token_type::value_unsigned)
|
||||
{
|
||||
value_unsigned = static_cast<number_unsigned_t>(w);
|
||||
// reject values that do not round-trip into a narrower type
|
||||
return (static_cast<std::uint64_t>(value_unsigned) == w) ? token_type::value_unsigned : token_type::uninitialized;
|
||||
}
|
||||
const std::int64_t x = -static_cast<std::int64_t>(w);
|
||||
value_integer = static_cast<number_integer_t>(x);
|
||||
return (static_cast<std::int64_t>(value_integer) == x) ? token_type::value_integer : token_type::uninitialized;
|
||||
}
|
||||
|
||||
/// contiguous input: try the number fast path, else the byte-path scanner
|
||||
token_type scan_number_dispatch(std::true_type /*bulk*/)
|
||||
{
|
||||
@@ -1770,18 +1855,23 @@ scan_number_done:
|
||||
}
|
||||
|
||||
/*!
|
||||
@param[in] literal_text the literal text to expect
|
||||
@param[in] length the length of the passed literal text
|
||||
@param[in] return_type the token type to return on success
|
||||
@param[in] rest the literal text after its first character, which
|
||||
is current (its length is a template parameter, so
|
||||
that the comparison of the contiguous fast path is
|
||||
a fixed-size load, not a call of memcmp)
|
||||
@param[in] return_type the token type to return on success
|
||||
*/
|
||||
JSON_HEDLEY_NON_NULL(2)
|
||||
token_type scan_literal(const char_type* literal_text, const std::size_t length,
|
||||
token_type return_type)
|
||||
template<std::size_t N>
|
||||
JSON_HEDLEY_NEVER_INLINE token_type scan_literal(const char (&rest)[N], token_type return_type) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
{
|
||||
JSON_ASSERT(char_traits<char_type>::to_char_type(current) == literal_text[0]);
|
||||
for (std::size_t i = 1; i < length; ++i)
|
||||
constexpr std::size_t length = N - 1;
|
||||
if (scan_literal_bulk(rest, length, std::integral_constant<bool, bulk_scan> {}))
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != literal_text[i]))
|
||||
return return_type;
|
||||
}
|
||||
for (std::size_t i = 0; i < length; ++i)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(char_traits<char_type>::to_char_type(get()) != static_cast<char_type>(rest[i])))
|
||||
{
|
||||
error_message = "invalid literal";
|
||||
return token_type::parse_error;
|
||||
@@ -1790,6 +1880,27 @@ scan_number_done:
|
||||
return return_type;
|
||||
}
|
||||
|
||||
/// contiguous input: compare the rest of a literal at once; false (and
|
||||
/// nothing consumed) if it does not match, so that the byte path reports
|
||||
/// the error at the right character
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool scan_literal_bulk(const char* rest, const std::size_t length, std::true_type /*bulk*/)
|
||||
{
|
||||
if (next_unget || ia.bulk_remaining() < length
|
||||
|| std::memcmp(ia.bulk_data(), rest, length) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bulk_consume(reinterpret_cast<const char*>(ia.bulk_data()), length);
|
||||
current = static_cast<char_int_type>(static_cast<unsigned char>(rest[length - 1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// streaming input: no bulk fast path
|
||||
bool scan_literal_bulk(const char* /*rest*/, const std::size_t /*length*/, std::false_type /*bulk*/) const noexcept
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// input management
|
||||
/////////////////////
|
||||
@@ -1798,6 +1909,7 @@ scan_number_done:
|
||||
void reset() noexcept
|
||||
{
|
||||
token_buffer.clear();
|
||||
pending_number_text = nullptr;
|
||||
decimal_point_position = std::string::npos;
|
||||
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
@@ -2057,10 +2169,30 @@ scan_number_done:
|
||||
/// return current string value
|
||||
string_t& get_string()
|
||||
{
|
||||
if (pending_number_text != nullptr)
|
||||
{
|
||||
token_buffer.append(reinterpret_cast<const typename string_t::value_type*>(pending_number_text), pending_number_length);
|
||||
pending_number_text = nullptr;
|
||||
}
|
||||
// a number token holds '.' regardless of the locale (#4084)
|
||||
return token_buffer;
|
||||
}
|
||||
|
||||
/// the value of a string token (key or string), which never has pending
|
||||
/// number text, so get_string()'s check is not needed
|
||||
string_t& get_string_token() noexcept
|
||||
{
|
||||
JSON_ASSERT(pending_number_text == nullptr);
|
||||
return token_buffer;
|
||||
}
|
||||
|
||||
/// the text of a float token for a SAX handler that ignores it (the
|
||||
/// library's own handlers): empty, without copying the token
|
||||
const string_t& get_ignored_number_text() const noexcept
|
||||
{
|
||||
return token_buffer;
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// diagnostics
|
||||
/////////////////////
|
||||
@@ -2394,21 +2526,14 @@ scan_number_done:
|
||||
return token_type::value_separator;
|
||||
|
||||
// literals
|
||||
// (the text after the first character; no local arrays, which
|
||||
// would give this function a stack canary on every token)
|
||||
case 't':
|
||||
{
|
||||
std::array<char_type, 4> true_literal = {{static_cast<char_type>('t'), static_cast<char_type>('r'), static_cast<char_type>('u'), static_cast<char_type>('e')}};
|
||||
return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true);
|
||||
}
|
||||
return scan_literal("rue", token_type::literal_true);
|
||||
case 'f':
|
||||
{
|
||||
std::array<char_type, 5> false_literal = {{static_cast<char_type>('f'), static_cast<char_type>('a'), static_cast<char_type>('l'), static_cast<char_type>('s'), static_cast<char_type>('e')}};
|
||||
return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false);
|
||||
}
|
||||
return scan_literal("alse", token_type::literal_false);
|
||||
case 'n':
|
||||
{
|
||||
std::array<char_type, 4> null_literal = {{static_cast<char_type>('n'), static_cast<char_type>('u'), static_cast<char_type>('l'), static_cast<char_type>('l')}};
|
||||
return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null);
|
||||
}
|
||||
return scan_literal("ull", token_type::literal_null);
|
||||
|
||||
// string
|
||||
case '\"':
|
||||
@@ -2456,6 +2581,11 @@ scan_number_done:
|
||||
/// whether the next get() call should just return current
|
||||
bool next_unget = false;
|
||||
|
||||
/// the text of the current float token in the input, not yet copied into
|
||||
/// token_buffer (see get_string()), or nullptr
|
||||
const char* pending_number_text = nullptr;
|
||||
std::size_t pending_number_length = 0;
|
||||
|
||||
/// the start position of the current token
|
||||
position_t position {};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // int64_t, uint64_t
|
||||
#include <cstdlib> // strtof, strtod, strtold
|
||||
#include <cstring> // memcpy
|
||||
#include <limits> // numeric_limits
|
||||
#include <string> // string
|
||||
#include <type_traits> // conditional, integral_constant, is_signed, true_type, false_type
|
||||
@@ -275,13 +274,77 @@ JSON_HEDLEY_ALWAYS_INLINE std::uint32_t parse_eight_digits(std::uint64_t v) noex
|
||||
return static_cast<std::uint32_t>(((v & 0x0000FFFF0000FFFFu) * 42949672960001u) >> 32u);
|
||||
}
|
||||
|
||||
/// whether [first, last) contains a digit other than '0'
|
||||
inline bool has_nonzero_digit(const char* first, const char* last) noexcept
|
||||
/// whether the eight bytes in @a v (see read_eight_bytes()) are all ASCII
|
||||
/// digits (after fast_float)
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool is_eight_digits(std::uint64_t v) noexcept
|
||||
{
|
||||
return std::any_of(first, last, [](char c) noexcept
|
||||
return ((v & 0xF0F0F0F0F0F0F0F0u) | (((v + 0x0606060606060606u) & 0xF0F0F0F0F0F0F0F0u) >> 4u)) == 0x3333333333333333u;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief skip the ASCII digits starting at data[i] and accumulate their value
|
||||
|
||||
Eight digits are taken at a time while at least eight bytes remain. @a w wraps
|
||||
around after 19 digits; the caller only uses it for shorter digit runs.
|
||||
|
||||
@return the index of the first byte at or after @a i that is not a digit
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t accumulate_digits(const char* data, std::size_t i, std::size_t avail, std::uint64_t& w) noexcept
|
||||
{
|
||||
while (avail - i >= 8)
|
||||
{
|
||||
return c != '0';
|
||||
});
|
||||
const std::uint64_t v = read_eight_bytes(data + i);
|
||||
if (!is_eight_digits(v))
|
||||
{
|
||||
break;
|
||||
}
|
||||
w = (w * 100000000u) + parse_eight_digits(v);
|
||||
i += 8;
|
||||
}
|
||||
while (i < avail && data[i] >= '0' && data[i] <= '9')
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(data[i] - '0');
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/// whether [first, last) contains a digit other than '0'; a plain loop, as
|
||||
/// GCC calls std::any_of out of line even for the usual empty range
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool has_nonzero_digit(const char* first, const char* last) noexcept
|
||||
{
|
||||
for (; first != last; ++first)
|
||||
{
|
||||
if (*first != '0')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief accumulate the validated digits [p, last) into @a w, as long as
|
||||
@a remaining digits still fit into it
|
||||
|
||||
Eight digits are taken at a time while at least eight remain, and @a remaining
|
||||
is decreased by the number of digits taken.
|
||||
|
||||
@return a pointer past the last digit taken
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE const char* accumulate_significant_digits(const char* p, const char* last, int& remaining, std::uint64_t& w) noexcept
|
||||
{
|
||||
while (remaining >= 8 && last - p >= 8)
|
||||
{
|
||||
w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p));
|
||||
p += 8;
|
||||
remaining -= 8;
|
||||
}
|
||||
for (; remaining > 0 && p != last; ++p, --remaining)
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(*p - '0');
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// the value of the validated exponent digits [+-]?[0-9]+ in [first, last),
|
||||
@@ -327,7 +390,7 @@ truncated is set.
|
||||
std::string::npos if there is none
|
||||
@param[in] mantissa_end index of the 'e'/'E', or the token length
|
||||
*/
|
||||
inline float_significand parse_float_significand(const char* first, const char* last,
|
||||
JSON_HEDLEY_ALWAYS_INLINE float_significand parse_float_significand(const char* first, const char* last,
|
||||
std::size_t decimal_point_position, std::size_t mantissa_end) noexcept
|
||||
{
|
||||
float_significand s;
|
||||
@@ -342,16 +405,7 @@ inline float_significand parse_float_significand(const char* first, const char*
|
||||
int remaining = 19; // digits that still fit into w
|
||||
if (*p != '0') // the integer part is "0" or [1-9][0-9]*
|
||||
{
|
||||
while (remaining >= 8 && integer_last - p >= 8)
|
||||
{
|
||||
w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p));
|
||||
p += 8;
|
||||
remaining -= 8;
|
||||
}
|
||||
for (; remaining > 0 && p != integer_last; ++p, --remaining)
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(*p - '0');
|
||||
}
|
||||
p = accumulate_significant_digits(p, integer_last, remaining, w);
|
||||
s.exponent = integer_last - p;
|
||||
s.truncated = has_nonzero_digit(p, integer_last);
|
||||
}
|
||||
@@ -370,16 +424,7 @@ inline float_significand parse_float_significand(const char* first, const char*
|
||||
s.exponent -= p - zeros;
|
||||
}
|
||||
const char* const digits = p;
|
||||
while (remaining >= 8 && mantissa_last - p >= 8)
|
||||
{
|
||||
w = (w * 100000000u) + parse_eight_digits(read_eight_bytes(p));
|
||||
p += 8;
|
||||
remaining -= 8;
|
||||
}
|
||||
for (; remaining > 0 && p != mantissa_last; ++p, --remaining)
|
||||
{
|
||||
w = (w * 10u) + static_cast<std::uint64_t>(*p - '0');
|
||||
}
|
||||
p = accumulate_significant_digits(p, mantissa_last, remaining, w);
|
||||
s.exponent -= p - digits;
|
||||
s.truncated = s.truncated || has_nonzero_digit(p, mantissa_last);
|
||||
}
|
||||
@@ -712,15 +757,14 @@ std::uint64_t digit_comparison(const char* first, const char* last, std::uint64_
|
||||
inline void float_from_bits(std::uint64_t bits, double& value) noexcept
|
||||
{
|
||||
static_assert(sizeof(double) == sizeof(std::uint64_t), "double must have 64 bits");
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
value = bit_cast<double>(bits);
|
||||
}
|
||||
|
||||
/// the float with the IEEE-754 bits @a bits (the lower 32)
|
||||
inline void float_from_bits(std::uint64_t bits, float& value) noexcept
|
||||
{
|
||||
static_assert(sizeof(float) == sizeof(std::uint32_t), "float must have 32 bits");
|
||||
const auto bits32 = static_cast<std::uint32_t>(bits);
|
||||
std::memcpy(&value, &bits32, sizeof(bits32));
|
||||
value = bit_cast<float>(static_cast<std::uint32_t>(bits));
|
||||
}
|
||||
|
||||
/// the powers of ten that are exact in binary64 (up to 10^22)
|
||||
|
||||
@@ -51,6 +51,20 @@ template<typename BasicJsonType>
|
||||
using parser_callback_t =
|
||||
std::function<bool(int /*depth*/, parse_event_t /*event*/, BasicJsonType& /*parsed*/)>;
|
||||
|
||||
/// whether a SAX handler ignores the text of float tokens (the library's own
|
||||
/// handlers do), so that the parser need not materialize it
|
||||
template<typename SAX>
|
||||
struct sax_ignores_number_text : std::false_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
struct sax_ignores_number_text<json_sax_dom_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
|
||||
|
||||
template<typename BasicJsonType, typename InputAdapterType>
|
||||
struct sax_ignores_number_text<json_sax_dom_callback_parser<BasicJsonType, InputAdapterType>> : std::true_type {};
|
||||
|
||||
template<typename BasicJsonType>
|
||||
struct sax_ignores_number_text<json_sax_acceptor<BasicJsonType>> : std::true_type {};
|
||||
|
||||
/*!
|
||||
@brief syntax analysis
|
||||
|
||||
@@ -171,6 +185,23 @@ class parser
|
||||
}
|
||||
|
||||
private:
|
||||
/// the text of the current float token for @a sax (see sax_ignores_number_text)
|
||||
template<typename SAX>
|
||||
const string_t& number_float_text(SAX* /*sax*/)
|
||||
{
|
||||
return number_float_text(std::integral_constant<bool, sax_ignores_number_text<SAX>::value> {});
|
||||
}
|
||||
|
||||
const string_t& number_float_text(std::true_type /*ignored*/) const noexcept
|
||||
{
|
||||
return m_lexer.get_ignored_number_text();
|
||||
}
|
||||
|
||||
string_t& number_float_text(std::false_type /*ignored*/)
|
||||
{
|
||||
return m_lexer.get_string();
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief run a DOM SAX parser to completion and position the lexer
|
||||
|
||||
@@ -213,8 +244,9 @@ class parser
|
||||
bool sax_parse_internal(SAX* sax)
|
||||
{
|
||||
// stack to remember the hierarchy of structured values we are parsing
|
||||
// true = array; false = object
|
||||
std::vector<bool> states;
|
||||
// true = array; false = object (bytes, not std::vector<bool>, whose
|
||||
// push_back() is a call with GCC)
|
||||
std::vector<std::uint8_t> states;
|
||||
// value to avoid a goto (see comment where set to true)
|
||||
bool skip_to_state_evaluation = false;
|
||||
|
||||
@@ -247,7 +279,7 @@ class parser
|
||||
{
|
||||
return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
|
||||
}
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -259,7 +291,7 @@ class parser
|
||||
}
|
||||
|
||||
// remember we are now inside an object
|
||||
states.push_back(false);
|
||||
states.push_back(0);
|
||||
|
||||
// parse values
|
||||
get_token();
|
||||
@@ -284,7 +316,7 @@ class parser
|
||||
}
|
||||
|
||||
// remember we are now inside an array
|
||||
states.push_back(true);
|
||||
states.push_back(1);
|
||||
|
||||
// parse values (no need to call get_token)
|
||||
continue;
|
||||
@@ -301,7 +333,7 @@ class parser
|
||||
out_of_range::create(exception_id::number_overflow, concat("number overflow parsing '", m_lexer.get_token_string(), '\''), nullptr));
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string())))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, number_float_text(sax))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -347,7 +379,7 @@ class parser
|
||||
|
||||
case token_type::value_string:
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string())))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string_token())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -401,7 +433,7 @@ class parser
|
||||
return true;
|
||||
}
|
||||
|
||||
if (states.back()) // array
|
||||
if (states.back() != 0) // array
|
||||
{
|
||||
// comma -> next value
|
||||
// or end of array (ignore_trailing_commas = true)
|
||||
@@ -456,7 +488,7 @@ class parser
|
||||
return syntax_error(*sax, exception_message(token_type::value_string, "object key"));
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string())))
|
||||
if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string_token())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-FileCopyrightText: 2018-2025 The simdjson authors <https://github.com/simdjson/simdjson>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array> // array
|
||||
#include <atomic> // atomic
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint8_t, uint64_t
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
// Vector code for runs of string bytes, used by the lexer for contiguous input
|
||||
// (see string_bulk_run()). NEON (AArch64) and SSE2 (x86-64) belong to the
|
||||
// baseline instruction sets and are used by default. The vector UTF-8 check
|
||||
// needs NEON or SSSE3. SSSE3 is not part of x86-64, and the code must not
|
||||
// depend on the flags of a translation unit (two translation units with
|
||||
// different flags would have different definitions of the same inline
|
||||
// functions): the check is compiled for SSSE3 with a function attribute and
|
||||
// used where the CPU has SSSE3 (all x86-64 CPUs since about 2011), else the
|
||||
// portable check. Code compiled for SSSE3 anyway (-mssse3 or more) skips the
|
||||
// CPU check. JSON_NO_SIMD selects the portable code everywhere.
|
||||
// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target
|
||||
// attribute and then reject the SSSE3 intrinsics) keeps the check portable
|
||||
// without disabling the SSE2 code.
|
||||
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#include <arm_neon.h>
|
||||
#define JSON_SCAN_NEON 1
|
||||
#else
|
||||
#define JSON_SCAN_NEON 0
|
||||
#endif
|
||||
// (x86 only: other targets can define __SSE2__ as well, e.g., WebAssembly with -msse2, but have no <cpuid.h>)
|
||||
#if !defined(JSON_NO_SIMD) && !JSON_SCAN_NEON && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86)) && !defined(_M_ARM64EC) && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2))
|
||||
#include <emmintrin.h>
|
||||
#define JSON_SCAN_SSE2 1
|
||||
#else
|
||||
#define JSON_SCAN_SSE2 0
|
||||
#endif
|
||||
#if JSON_SCAN_SSE2 && defined(__SSSE3__)
|
||||
#include <tmmintrin.h>
|
||||
#define JSON_SCAN_SSSE3 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#else
|
||||
#define JSON_SCAN_SSSE3 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#endif
|
||||
#if JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && !defined(NLOHMANN_JSON_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9)) && !defined(__cpp_modules)))
|
||||
// (GCC before 4.9 has no SSSE3 intrinsics without -mssse3; GCC with C++20
|
||||
// modules drops the target attribute from functions it reads from a module
|
||||
// interface and then rejects the intrinsics: "needs isa option -mssse3")
|
||||
// <cpuid.h> of GCC before 11 has no include guard, and json.hpp and
|
||||
// json_view.hpp both include it
|
||||
#ifndef NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#define NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
#include <tmmintrin.h>
|
||||
#define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_SCAN_SSSE3_TARGET __attribute__((target("ssse3")))
|
||||
#elif JSON_SCAN_SSE2 && !JSON_SCAN_SSSE3 && defined(_MSC_VER)
|
||||
// (MSVC compiles intrinsics of any instruction set)
|
||||
#include <intrin.h>
|
||||
#include <tmmintrin.h>
|
||||
#define JSON_SCAN_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_SCAN_SSSE3_TARGET
|
||||
#else
|
||||
#define JSON_SCAN_SSSE3_DISPATCH 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_SCAN_SSSE3_TARGET
|
||||
#endif
|
||||
#define JSON_SCAN_VECTOR (JSON_SCAN_NEON || JSON_SCAN_SSE2)
|
||||
#define JSON_SCAN_VECTOR_UTF8 (JSON_SCAN_NEON || JSON_SCAN_SSSE3 || JSON_SCAN_SSSE3_DISPATCH)
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
{
|
||||
|
||||
#if JSON_SCAN_VECTOR
|
||||
/*!
|
||||
@brief the first byte of a string run that is a quote, a backslash, a control
|
||||
character, or not ASCII, 16 bytes per step
|
||||
|
||||
Stops at such a byte, or where fewer than 16 bytes are left (the caller tells
|
||||
the two apart). A signed compare with 0x20 finds control characters and
|
||||
non-ASCII bytes at once.
|
||||
*/
|
||||
JSON_HEDLEY_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (e - p >= 16)
|
||||
{
|
||||
#if JSON_SCAN_NEON
|
||||
const uint8x16_t in = vld1q_u8(p);
|
||||
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))),
|
||||
vcltq_s8(vreinterpretq_s8_u8(in), vdupq_n_s8(0x20)));
|
||||
// one nibble per byte (the usual NEON replacement of x86's movemask, see
|
||||
// D. Kutenin, "Porting x86 vector bitmask optimizations to Arm NEON", 2022)
|
||||
const std::uint64_t bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
|
||||
if (bits != 0)
|
||||
{
|
||||
return p + (count_trailing_zeros(bits) >> 2u);
|
||||
}
|
||||
#else
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_cmplt_epi8(in, _mm_set1_epi8(0x20)));
|
||||
const auto bits = static_cast<std::uint64_t>(static_cast<unsigned>(_mm_movemask_epi8(special)));
|
||||
if (bits != 0)
|
||||
{
|
||||
return p + count_trailing_zeros(bits);
|
||||
}
|
||||
#endif
|
||||
p += 16;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_VECTOR
|
||||
/// vector_plain_run() for runs that are already longer than a few words:
|
||||
/// inline with SSE2, out of line with NEON, where a larger string loop slows
|
||||
/// down the rest of the lexer
|
||||
#if JSON_SCAN_NEON
|
||||
JSON_HEDLEY_NEVER_INLINE inline
|
||||
#else
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
#endif
|
||||
const unsigned char* vector_plain_run_long(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
return vector_plain_run(p, e);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_SSSE3_DISPATCH
|
||||
/// whether the CPU has SSSE3 (CPUID leaf 1, ECX bit 9)
|
||||
inline bool cpu_ssse3() noexcept
|
||||
{
|
||||
#if defined(_MSC_VER) && !defined(__clang__)
|
||||
std::array<int, 4> regs {{}};
|
||||
__cpuid(regs.data(), 1);
|
||||
return (static_cast<unsigned>(regs[2]) & (1u << 9u)) != 0;
|
||||
#else
|
||||
unsigned eax = 0;
|
||||
unsigned ebx = 0;
|
||||
unsigned ecx = 0;
|
||||
unsigned edx = 0;
|
||||
return __get_cpuid(1, &eax, &ebx, &ecx, &edx) != 0 && (ecx & (1u << 9u)) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// whether the CPU has SSSE3, asked once: the answer is kept in an atomic
|
||||
/// that is initialized at compile time, so that neither a guard of a local
|
||||
/// static nor a global constructor is needed (threads that ask at the same
|
||||
/// time all store the same answer)
|
||||
JSON_HEDLEY_ALWAYS_INLINE bool cpu_has_ssse3() noexcept
|
||||
{
|
||||
static std::atomic<int> known{0}; // 0: not asked yet, 1: no, 2: yes
|
||||
int state = known.load(std::memory_order_relaxed);
|
||||
if (JSON_HEDLEY_UNLIKELY(state == 0))
|
||||
{
|
||||
state = cpu_ssse3() ? 2 : 1;
|
||||
known.store(state, std::memory_order_relaxed);
|
||||
}
|
||||
return state == 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
/// Tables of the UTF-8 check of J. Keiser and D. Lemire, "Validating UTF-8 In
|
||||
/// Less Than One Instruction Per Byte" (2021), as in simdjson ("lookup4"): each
|
||||
/// maps a nibble (high and low nibble of the previous byte, high nibble of the
|
||||
/// current byte) to the errors it allows; a byte pair is ill-formed if all
|
||||
/// three have an error bit in common.
|
||||
template<typename Dummy = void>
|
||||
struct utf8_lookup4
|
||||
{
|
||||
static constexpr std::uint8_t too_short = 1u << 0u, too_long = 1u << 1u, overlong_3 = 1u << 2u, too_large = 1u << 3u;
|
||||
static constexpr std::uint8_t surrogate = 1u << 4u, overlong_2 = 1u << 5u, too_large_1000 = 1u << 6u, overlong_4 = 1u << 6u;
|
||||
static constexpr std::uint8_t two_conts = 1u << 7u, carry = too_short | too_long | two_conts;
|
||||
static const std::array<std::uint8_t, 16> byte_1_high;
|
||||
static const std::array<std::uint8_t, 16> byte_1_low;
|
||||
static const std::array<std::uint8_t, 16> byte_2_high;
|
||||
};
|
||||
|
||||
template<typename Dummy>
|
||||
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_1_high =
|
||||
{
|
||||
{
|
||||
too_long, too_long, too_long, too_long, too_long, too_long, too_long, too_long,
|
||||
two_conts, two_conts, two_conts, two_conts,
|
||||
too_short | overlong_2, too_short, too_short | overlong_3 | surrogate, too_short | too_large | too_large_1000 | overlong_4
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Dummy>
|
||||
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_1_low =
|
||||
{
|
||||
{
|
||||
carry | overlong_3 | overlong_2 | overlong_4, carry | overlong_2, carry, carry,
|
||||
carry | too_large, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000,
|
||||
carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000, carry | too_large | too_large_1000,
|
||||
carry | too_large | too_large_1000, carry | too_large | too_large_1000 | surrogate, carry | too_large | too_large_1000, carry | too_large | too_large_1000
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Dummy>
|
||||
const std::array<std::uint8_t, 16> utf8_lookup4<Dummy>::byte_2_high =
|
||||
{
|
||||
{
|
||||
too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short,
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4),
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | overlong_3 | too_large),
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | surrogate | too_large),
|
||||
static_cast<std::uint8_t>(too_long | overlong_2 | two_conts | surrogate | too_large),
|
||||
too_short, too_short, too_short, too_short
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief the run of plain string bytes and well-formed UTF-8 at p (a character
|
||||
boundary), 16 bytes per step
|
||||
|
||||
The first quote, backslash, or control character is found with vector
|
||||
compares, and the UTF-8 check of J. Keiser and D. Lemire covers the bytes up
|
||||
to it. Returns true with @a block at that byte; returns false with @a block at
|
||||
the start of the 16 bytes where the check found ill-formed UTF-8 or where
|
||||
fewer than 16 bytes are left, for the caller to finish one sequence at a time.
|
||||
Out of line, so that no constants of the check occupy registers in the
|
||||
caller's loop. On x86-64, it is compiled for SSSE3 (see cpu_has_ssse3()).
|
||||
*/
|
||||
JSON_SCAN_SSSE3_TARGET JSON_HEDLEY_NEVER_INLINE inline bool vector_utf8_run(const unsigned char* p, const unsigned char* e, const unsigned char*& block_out) noexcept
|
||||
{
|
||||
using lookup = utf8_lookup4<>;
|
||||
const unsigned char* block = p;
|
||||
#if JSON_SCAN_NEON
|
||||
const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high.data());
|
||||
const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low.data());
|
||||
const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high.data());
|
||||
uint8x16_t prev = vdupq_n_u8(0);
|
||||
while (e - block >= 16)
|
||||
{
|
||||
const uint8x16_t in = vld1q_u8(block);
|
||||
const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(in, vdupq_n_u8('"')), vceqq_u8(in, vdupq_n_u8('\\'))), vcltq_u8(in, vdupq_n_u8(0x20)));
|
||||
const uint8x16_t prev1 = vextq_u8(prev, in, 15);
|
||||
const uint8x16_t sc = vandq_u8(vandq_u8(vqtbl1q_u8(t1h, vshrq_n_u8(prev1, 4)), vqtbl1q_u8(t1l, vandq_u8(prev1, vdupq_n_u8(0x0F)))), vqtbl1q_u8(t2h, vshrq_n_u8(in, 4)));
|
||||
const uint8x16_t must23 = vorrq_u8(vqsubq_u8(vextq_u8(prev, in, 14), vdupq_n_u8(0xE0 - 0x80)), vqsubq_u8(vextq_u8(prev, in, 13), vdupq_n_u8(0xF0 - 0x80)));
|
||||
const uint8x16_t err = veorq_u8(vandq_u8(must23, vdupq_n_u8(0x80)), sc);
|
||||
const std::uint64_t special_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(special), 4)), 0);
|
||||
const std::uint64_t err_bits = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(vtstq_u8(err, err)), 4)), 0);
|
||||
if (special_bits != 0)
|
||||
{
|
||||
// errors up to the special byte count (an incomplete sequence
|
||||
// before a quote shows at the quote); the bytes after it do not
|
||||
const unsigned k = static_cast<unsigned>(count_trailing_zeros(special_bits)) >> 2u;
|
||||
const std::uint64_t upto = k == 15 ? ~std::uint64_t{0} :
|
||||
(std::uint64_t{1} << (4u * (k + 1u))) - 1u;
|
||||
if ((err_bits & upto) == 0)
|
||||
{
|
||||
block_out = block + k;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (err_bits != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
prev = in;
|
||||
block += 16;
|
||||
}
|
||||
#else
|
||||
// the same with SSSE3 (pshufb for the table lookups; nibbles from 16-bit
|
||||
// shifts, as there are no byte shifts)
|
||||
const __m128i t1h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_high.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i t1l = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_1_low.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i t2h = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(lookup::byte_2_high.data()))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i nibble = _mm_set1_epi8(0x0F);
|
||||
const __m128i zero = _mm_setzero_si128();
|
||||
__m128i prev = zero;
|
||||
while (e - block >= 16)
|
||||
{
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(block))); // NOLINT(bugprone-casting-through-void)
|
||||
const __m128i special = _mm_or_si128(_mm_or_si128(_mm_cmpeq_epi8(in, _mm_set1_epi8('"')), _mm_cmpeq_epi8(in, _mm_set1_epi8('\\'))),
|
||||
_mm_cmpeq_epi8(_mm_subs_epu8(in, _mm_set1_epi8(0x1F)), zero)); // in < 0x20
|
||||
const __m128i prev1 = _mm_alignr_epi8(in, prev, 15);
|
||||
const __m128i sc = _mm_and_si128(_mm_and_si128(_mm_shuffle_epi8(t1h, _mm_and_si128(_mm_srli_epi16(prev1, 4), nibble)),
|
||||
_mm_shuffle_epi8(t1l, _mm_and_si128(prev1, nibble))),
|
||||
_mm_shuffle_epi8(t2h, _mm_and_si128(_mm_srli_epi16(in, 4), nibble)));
|
||||
const __m128i must23 = _mm_or_si128(_mm_subs_epu8(_mm_alignr_epi8(in, prev, 14), _mm_set1_epi8(0xE0 - 0x80)),
|
||||
_mm_subs_epu8(_mm_alignr_epi8(in, prev, 13), _mm_set1_epi8(0xF0 - 0x80)));
|
||||
const __m128i err = _mm_xor_si128(_mm_and_si128(must23, _mm_set1_epi8(static_cast<char>(-128))), sc);
|
||||
const auto special_bits = static_cast<unsigned>(_mm_movemask_epi8(special));
|
||||
const auto err_bits = ~static_cast<unsigned>(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu;
|
||||
if (special_bits != 0)
|
||||
{
|
||||
const auto k = static_cast<unsigned>(count_trailing_zeros(static_cast<std::uint64_t>(special_bits)));
|
||||
if ((err_bits & ((2u << k) - 1u)) == 0)
|
||||
{
|
||||
block_out = block + k;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (err_bits != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
prev = in;
|
||||
block += 16;
|
||||
}
|
||||
#endif
|
||||
block_out = block;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace detail
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/input/simd_scan.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
// Optional SIMD backend for bulk UTF-8 validation. This is an opt-in external
|
||||
@@ -34,7 +35,7 @@
|
||||
// This file contains the byte-level string-scanning helpers used by the lexer's
|
||||
// contiguous fast path. They operate purely on raw bytes (no dependency on the
|
||||
// lexer's template parameters) so they are free functions, keeping the lexer
|
||||
// itself focused on the state machine; see lexer::scan_string_bulk().
|
||||
// itself focused on the state machine; see lexer::get_after_string_run().
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
@@ -66,7 +67,7 @@ inline std::uint64_t swar_string_special(std::uint64_t v) noexcept
|
||||
|
||||
// return the index of the first is_string_special() byte in [data, data+n), or
|
||||
// n if every byte is ordinary; scans 8 bytes at a time
|
||||
inline std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t find_string_special(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
@@ -235,35 +236,143 @@ inline std::size_t valid_utf8_prefix(const unsigned char* data, std::size_t n) n
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Scalar (C++11) computation of the bulk run length: the number of leading
|
||||
// bytes in [data, data+n) that are ordinary ASCII or complete well-formed UTF-8
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
/// the end of scan_string_vector from block, where the vector loop stopped
|
||||
/// (ill-formed UTF-8, or fewer than 16 bytes left): one byte or sequence at a
|
||||
/// time, from the start of a sequence that crosses into the block
|
||||
inline const unsigned char* scan_string_finish(const unsigned char* p, const unsigned char* block, const unsigned char* e) noexcept
|
||||
{
|
||||
for (int i = 1; i <= 3 && block - i >= p; ++i)
|
||||
{
|
||||
const unsigned char c = block[-i];
|
||||
if (c < 0x80)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (c >= 0xC0)
|
||||
{
|
||||
const int len = 2 + static_cast<int>(c >= 0xE0) + static_cast<int>(c >= 0xF0);
|
||||
if (len > i)
|
||||
{
|
||||
block -= i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (p = block; p != e;)
|
||||
{
|
||||
if (*p < 0x80)
|
||||
{
|
||||
if (is_string_special(*p))
|
||||
{
|
||||
return p;
|
||||
}
|
||||
++p;
|
||||
continue;
|
||||
}
|
||||
const std::size_t n = validate_one_utf8(p, static_cast<std::size_t>(e - p));
|
||||
if (n == 0)
|
||||
{
|
||||
return p;
|
||||
}
|
||||
p += n;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// the rest of a string from p (a character boundary), with the vector UTF-8
|
||||
/// check and the scalar check for the end of the block where it stopped
|
||||
inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
const unsigned char* block = p;
|
||||
if (vector_utf8_run(p, e, block))
|
||||
{
|
||||
return block;
|
||||
}
|
||||
return scan_string_finish(p, block, e);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if JSON_SCAN_VECTOR
|
||||
/// the end of the run of plain string characters at @a p: the first quote,
|
||||
/// backslash, control character, or non-ASCII byte, or @a e. The first words
|
||||
/// are checked with SWAR (one; four with NEON, where the vector mask costs
|
||||
/// more), as most keys and short values end there; longer runs continue 16
|
||||
/// bytes at a time.
|
||||
JSON_HEDLEY_ALWAYS_INLINE const unsigned char* plain_run_end(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
constexpr int prefix_words = JSON_SCAN_NEON ? 4 : 1;
|
||||
for (int words = prefix_words; words > 0 && e - p >= 8; --words)
|
||||
{
|
||||
const std::uint64_t special = swar_string_special(read_eight_bytes(p));
|
||||
if (special != 0)
|
||||
{
|
||||
return p + (static_cast<std::size_t>(count_trailing_zeros(special)) / 8);
|
||||
}
|
||||
p += 8;
|
||||
}
|
||||
// the vector loop only for 16 bytes or more: the serializer passes a
|
||||
// string without its closing quote, so most of its runs end at e, and a
|
||||
// call of the out-of-line NEON loop for a shorter rest made dump() up to
|
||||
// 10% slower
|
||||
if (e - p >= 16)
|
||||
{
|
||||
p = vector_plain_run_long(p, e);
|
||||
if (e - p >= 16)
|
||||
{
|
||||
return p; // the vector loop stopped at a special byte
|
||||
}
|
||||
}
|
||||
return p + find_string_special(p, static_cast<std::size_t>(e - p));
|
||||
}
|
||||
#endif
|
||||
|
||||
// Computation of the bulk run length: the number of leading bytes in
|
||||
// [data, data+n) that are ordinary ASCII or complete well-formed UTF-8
|
||||
// sequences, stopping before the first byte that needs individual handling (the
|
||||
// closing quote, an escape, a control character, or an ill-formed/truncated
|
||||
// sequence). ASCII is skipped 8 bytes at a time.
|
||||
inline std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
// sequence). ASCII is skipped 16 bytes at a time with SSE2 and 8 bytes at a
|
||||
// time otherwise; runs of non-ASCII text use the vector UTF-8 check where
|
||||
// available.
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t scalar_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
std::size_t pos = 0;
|
||||
while (pos < n)
|
||||
const unsigned char* const e = data + n;
|
||||
const unsigned char* p = data;
|
||||
while (p != e)
|
||||
{
|
||||
pos += find_string_special(data + pos, n - pos);
|
||||
if (pos >= n || data[pos] < 0x80u)
|
||||
#if JSON_SCAN_VECTOR
|
||||
p = plain_run_end(p, e);
|
||||
#else
|
||||
p += find_string_special(p, static_cast<std::size_t>(e - p));
|
||||
#endif
|
||||
if (p == e || *p < 0x80u)
|
||||
{
|
||||
break; // end of buffer, or a quote/escape/control byte
|
||||
}
|
||||
#if JSON_SCAN_VECTOR_UTF8
|
||||
#if JSON_SCAN_SSSE3_DISPATCH
|
||||
if (JSON_HEDLEY_LIKELY(cpu_has_ssse3()))
|
||||
#endif
|
||||
{
|
||||
return static_cast<std::size_t>(scan_string_vector(p, e) - data);
|
||||
}
|
||||
#endif
|
||||
#if !JSON_SCAN_VECTOR_UTF8 || JSON_SCAN_SSSE3_DISPATCH
|
||||
// a run of multi-byte sequences (e.g. CJK text) is validated sequence
|
||||
// by sequence without searching for the next special byte in between
|
||||
do
|
||||
{
|
||||
const std::size_t seq = validate_one_utf8(data + pos, n - pos);
|
||||
const std::size_t seq = validate_one_utf8(p, static_cast<std::size_t>(e - p));
|
||||
if (seq == 0)
|
||||
{
|
||||
return pos; // ill-formed or truncated: let the byte path diagnose it
|
||||
return static_cast<std::size_t>(p - data); // ill-formed or truncated: let the byte path diagnose it
|
||||
}
|
||||
pos += seq;
|
||||
p += seq;
|
||||
}
|
||||
while (pos < n && data[pos] >= 0x80u);
|
||||
while (p != e && *p >= 0x80u);
|
||||
#endif
|
||||
}
|
||||
return pos;
|
||||
return static_cast<std::size_t>(p - data);
|
||||
}
|
||||
|
||||
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
|
||||
@@ -304,7 +413,7 @@ inline std::size_t find_string_delimiter(const unsigned char* data, std::size_t
|
||||
// next delimiter is validated in one shot by simdutf; on the rare failure the
|
||||
// scalar helper recomputes the exact valid prefix so the byte path still
|
||||
// produces the precise diagnostic. Without it, the pure scalar path is used.
|
||||
inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
#if defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17)
|
||||
const std::size_t run = find_string_delimiter(data, n);
|
||||
@@ -316,6 +425,42 @@ inline std::size_t string_bulk_run(const unsigned char* data, std::size_t n) noe
|
||||
return scalar_string_bulk_run(data, n);
|
||||
}
|
||||
|
||||
#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17))
|
||||
/// scalar_string_bulk_run() from @a start on, out of line (see
|
||||
/// lexer_string_bulk_run())
|
||||
JSON_HEDLEY_NEVER_INLINE inline std::size_t string_bulk_run_from(const unsigned char* data, std::size_t start, std::size_t n) noexcept
|
||||
{
|
||||
return start + scalar_string_bulk_run(data + start, n - start);
|
||||
}
|
||||
#endif
|
||||
|
||||
// string_bulk_run() for the lexer. With NEON, only the first four words are
|
||||
// checked inline (most keys and short values end there), and longer runs and
|
||||
// non-ASCII text continue out of line. The lexer's scan function runs once per
|
||||
// token, and the vector code inlined into it needs more registers, which every
|
||||
// token pays for: 5% of json::accept() on jeopardy.json with Apple Clang on
|
||||
// Apple M1, even though no string of that file reaches the vector code. The
|
||||
// serializer keeps string_bulk_run() inline, which is faster there.
|
||||
JSON_HEDLEY_ALWAYS_INLINE std::size_t lexer_string_bulk_run(const unsigned char* data, std::size_t n) noexcept
|
||||
{
|
||||
#if JSON_SCAN_NEON && !(defined(JSON_USE_SIMDUTF) && defined(JSON_HAS_CPP_17))
|
||||
std::size_t i = 0;
|
||||
for (int words = 4; words > 0 && n - i >= 8; --words)
|
||||
{
|
||||
const std::uint64_t special = swar_string_special(read_eight_bytes(data + i));
|
||||
if (special != 0)
|
||||
{
|
||||
const std::size_t pos = i + (static_cast<std::size_t>(count_trailing_zeros(special)) / 8);
|
||||
return data[pos] < 0x80u ? pos : string_bulk_run_from(data, pos, n);
|
||||
}
|
||||
i += 8;
|
||||
}
|
||||
return string_bulk_run_from(data, i, n);
|
||||
#else
|
||||
return string_bulk_run(data, n);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Decode the 4 hex digits at [data, data+4) - the digits following a `\u`
|
||||
// escape - into a codepoint 0x0000..0xFFFF via one table lookup per byte
|
||||
// (after yyjson's read_hex_u16), or return -1 if any of the 4 bytes is not a
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -187,6 +187,22 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// the byte order of the target where the compiler tells it (std::endian is
|
||||
// C++20 and cannot select code in #if): JSON_LITTLE_ENDIAN_TARGET or
|
||||
// JSON_BIG_ENDIAN_TARGET is 1, or both are 0 where it is unknown (code then
|
||||
// handles the bytes one at a time); MSVC and x86 targets are little-endian
|
||||
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__) \
|
||||
|| (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
|
||||
#define JSON_LITTLE_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
#define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_BIG_ENDIAN_TARGET 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#else
|
||||
#define JSON_LITTLE_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define JSON_BIG_ENDIAN_TARGET 0 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#define JSON_INLINE_VARIABLE inline
|
||||
#else
|
||||
|
||||
@@ -24,6 +24,15 @@
|
||||
#undef JSON_UNLOCKED_FILE_IO
|
||||
#undef JSON_DTOA_SSE2
|
||||
#undef JSON_DTOA_NEON
|
||||
#undef JSON_LITTLE_ENDIAN_TARGET
|
||||
#undef JSON_BIG_ENDIAN_TARGET
|
||||
#undef JSON_SCAN_NEON
|
||||
#undef JSON_SCAN_SSE2
|
||||
#undef JSON_SCAN_SSSE3
|
||||
#undef JSON_SCAN_SSSE3_DISPATCH
|
||||
#undef JSON_SCAN_SSSE3_TARGET
|
||||
#undef JSON_SCAN_VECTOR
|
||||
#undef JSON_SCAN_VECTOR_UTF8
|
||||
|
||||
#ifndef JSON_TEST_KEEP_MACROS
|
||||
#undef JSON_CATCH
|
||||
|
||||
@@ -10,17 +10,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef> // size_t
|
||||
#include <type_traits> // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type
|
||||
#include <type_traits> // conditional, enable_if, false_type, integral_constant, is_arithmetic, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type
|
||||
#include <utility> // index_sequence, make_index_sequence, index_sequence_for
|
||||
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
|
||||
#ifdef JSON_HAS_CPP_17
|
||||
#include <algorithm> // clamp
|
||||
#endif
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
namespace detail
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
using uncvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
||||
#ifdef __cpp_lib_remove_cvref
|
||||
template<typename T>
|
||||
using uncvref_t = std::remove_cvref_t<T>;
|
||||
#else
|
||||
template<typename T>
|
||||
using uncvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
|
||||
#endif
|
||||
|
||||
#ifdef JSON_HAS_CPP_14
|
||||
|
||||
@@ -160,5 +169,19 @@ struct static_const
|
||||
constexpr T static_const<T>::value;
|
||||
#endif
|
||||
|
||||
/// @a v bounded to [@a lo, @a hi] (requires !(hi < lo)); like std::clamp,
|
||||
/// but by value, so that static constexpr members can be passed without an
|
||||
/// out-of-line definition before C++17
|
||||
template<typename T, enable_if_t<std::is_arithmetic<T>::value, int> = 0>
|
||||
constexpr T clamp(T v, T lo, T hi) noexcept
|
||||
{
|
||||
#ifdef __cpp_lib_clamp
|
||||
return std::clamp(v, lo, hi);
|
||||
#else
|
||||
// NOLINTNEXTLINE(readability-avoid-nested-conditional-operator): a constant expression of C++11
|
||||
return v < lo ? lo : (hi < v ? hi : v);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
@@ -313,6 +313,14 @@ struct char_traits<std::byte> : std::char_traits<char>
|
||||
// is_ functions //
|
||||
///////////////////
|
||||
|
||||
#ifdef __cpp_lib_logical_traits
|
||||
|
||||
using std::conjunction;
|
||||
using std::disjunction;
|
||||
using std::negation;
|
||||
|
||||
#else
|
||||
|
||||
// https://en.cppreference.com/w/cpp/types/conjunction
|
||||
template<class...> struct conjunction : std::true_type { };
|
||||
template<class B> struct conjunction<B> : B { };
|
||||
@@ -330,6 +338,8 @@ struct disjunction<B, Bn...>
|
||||
// https://en.cppreference.com/w/cpp/types/negation
|
||||
template<class B> struct negation : std::integral_constant < bool, !B::value > { };
|
||||
|
||||
#endif
|
||||
|
||||
// Reimplementation of is_constructible and is_default_constructible, due to them being broken for
|
||||
// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367).
|
||||
// This causes compile errors in e.g., Clang 3.5 or GCC 4.9.
|
||||
@@ -1060,8 +1070,12 @@ constexpr bool value_in_range_of(T val)
|
||||
return value_in_range_of_impl1<OfType, T>::test(val);
|
||||
}
|
||||
|
||||
template<bool Value>
|
||||
using bool_constant = std::integral_constant<bool, Value>;
|
||||
#ifdef __cpp_lib_bool_constant
|
||||
using std::bool_constant;
|
||||
#else
|
||||
template<bool Value>
|
||||
using bool_constant = std::integral_constant<bool, Value>;
|
||||
#endif
|
||||
|
||||
// Stands in for number_unsigned_t in overload sets if it is the same type as
|
||||
// number_integer_t, so that the unsigned overloads do not collide with the
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -55,6 +55,66 @@ using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
|
||||
/// output_vector_sink also appends writes of at least this size with insert()
|
||||
constexpr std::size_t max_reserve_size = 256;
|
||||
|
||||
/// @brief copy @a length characters, at most 32, without a call of memcpy()
|
||||
///
|
||||
/// Short strings are the typical payload of a binary value, and a call of
|
||||
/// memcpy() costs more than copying them. A copy of 16 to 32 characters is two
|
||||
/// possibly overlapping 16-byte copies, one of 8 to 15 two 8-byte copies, and
|
||||
/// so on down to a single character; each is a fixed-size load and store.
|
||||
/// Nothing outside of [src, src + length) is read or written.
|
||||
template<typename CharType>
|
||||
inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::true_type /*is_byte*/) noexcept
|
||||
{
|
||||
JSON_ASSERT(length <= 32);
|
||||
if (length >= 16)
|
||||
{
|
||||
std::array<char, 16> first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<char, 16> last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::memcpy(first.data(), src, 16);
|
||||
std::memcpy(last.data(), src + (length - 16), 16);
|
||||
std::memcpy(dst, first.data(), 16);
|
||||
std::memcpy(dst + (length - 16), last.data(), 16);
|
||||
}
|
||||
else if (length >= 8)
|
||||
{
|
||||
std::array<char, 8> first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<char, 8> last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::memcpy(first.data(), src, 8);
|
||||
std::memcpy(last.data(), src + (length - 8), 8);
|
||||
std::memcpy(dst, first.data(), 8);
|
||||
std::memcpy(dst + (length - 8), last.data(), 8);
|
||||
}
|
||||
else if (length >= 4)
|
||||
{
|
||||
std::array<char, 4> first; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::array<char, 4> last; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::memcpy(first.data(), src, 4);
|
||||
std::memcpy(last.data(), src + (length - 4), 4);
|
||||
std::memcpy(dst, first.data(), 4);
|
||||
std::memcpy(dst + (length - 4), last.data(), 4);
|
||||
}
|
||||
else if (length != 0)
|
||||
{
|
||||
// 1 to 3: the first, the middle, and the last character
|
||||
const CharType a = src[0];
|
||||
const CharType b = src[length >> 1];
|
||||
const CharType c = src[length - 1];
|
||||
dst[0] = a;
|
||||
dst[length >> 1] = b;
|
||||
dst[length - 1] = c;
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief copy @a length characters, of a character type wider than a byte
|
||||
template<typename CharType>
|
||||
inline void copy_short(CharType* dst, const CharType* src, const std::size_t length, std::false_type /*is_byte*/) noexcept
|
||||
{
|
||||
std::memcpy(dst, src, length * sizeof(CharType));
|
||||
}
|
||||
|
||||
/// the number of characters an empty vector is grown to by output_vector_sink
|
||||
constexpr std::size_t min_vector_sink_capacity = 64;
|
||||
|
||||
/// @brief non-virtual output sink writing into a std::vector
|
||||
///
|
||||
/// This sink is not part of the virtual output_adapter_protocol hierarchy: it is
|
||||
@@ -78,7 +138,7 @@ class output_vector_sink
|
||||
{
|
||||
public:
|
||||
explicit output_vector_sink(std::vector<CharType, AllocatorType>& vec) noexcept
|
||||
: v(&vec), position(vec.size())
|
||||
: v(&vec), cursor(vec.data() + vec.size()), limit(cursor)
|
||||
{}
|
||||
|
||||
output_vector_sink(const output_vector_sink&) = delete;
|
||||
@@ -86,7 +146,7 @@ class output_vector_sink
|
||||
output_vector_sink& operator=(output_vector_sink&&) = delete;
|
||||
|
||||
output_vector_sink(output_vector_sink&& other) noexcept
|
||||
: v(other.v), position(other.position)
|
||||
: v(other.v), cursor(other.cursor), limit(other.limit)
|
||||
{
|
||||
other.v = nullptr;
|
||||
}
|
||||
@@ -95,7 +155,7 @@ class output_vector_sink
|
||||
{
|
||||
if (v != nullptr)
|
||||
{
|
||||
v->resize(position);
|
||||
v->resize(position());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,11 +164,11 @@ class output_vector_sink
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
void write_character(CharType c)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(position == v->size()))
|
||||
if (JSON_HEDLEY_UNLIKELY(cursor == limit))
|
||||
{
|
||||
grow(1);
|
||||
}
|
||||
(*v)[position++] = c;
|
||||
*cursor++ = c;
|
||||
}
|
||||
|
||||
// no JSON_HEDLEY_NON_NULL here: binary_writer legitimately passes a null
|
||||
@@ -117,7 +177,7 @@ class output_vector_sink
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
void write_characters(const CharType* s, std::size_t length)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
|
||||
if (JSON_HEDLEY_UNLIKELY(space() < length))
|
||||
{
|
||||
if (length >= max_reserve_size)
|
||||
{
|
||||
@@ -126,10 +186,15 @@ class output_vector_sink
|
||||
}
|
||||
grow(length);
|
||||
}
|
||||
if (length != 0)
|
||||
if (length <= 32)
|
||||
{
|
||||
std::memcpy(v->data() + position, s, length * sizeof(CharType));
|
||||
position += length;
|
||||
copy_short(cursor, s, length, std::integral_constant<bool, sizeof(CharType) == 1>());
|
||||
cursor += length;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::memcpy(cursor, s, length * sizeof(CharType));
|
||||
cursor += length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,21 +209,37 @@ class output_vector_sink
|
||||
CharType* reserve(const std::size_t length)
|
||||
{
|
||||
JSON_ASSERT(length <= max_reserve_size);
|
||||
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
|
||||
if (JSON_HEDLEY_UNLIKELY(space() < length))
|
||||
{
|
||||
grow(length);
|
||||
}
|
||||
return v->data() + position;
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/// keep the first @a length characters written to the space from reserve()
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
void commit(const std::size_t length) noexcept
|
||||
{
|
||||
position += length;
|
||||
cursor += length;
|
||||
}
|
||||
|
||||
/// nothing to pass on: the characters are in the vector (see output_adapter_sink)
|
||||
void flush() noexcept
|
||||
{}
|
||||
|
||||
private:
|
||||
/// number of characters of *v written so far
|
||||
std::size_t position() const noexcept
|
||||
{
|
||||
return static_cast<std::size_t>(cursor - v->data());
|
||||
}
|
||||
|
||||
/// number of characters that fit the vector behind the write position
|
||||
std::size_t space() const noexcept
|
||||
{
|
||||
return static_cast<std::size_t>(limit - cursor);
|
||||
}
|
||||
|
||||
/// make room for at least @a length more characters after the position;
|
||||
/// kept out of line so that the write functions stay small enough for
|
||||
/// GCC to inline them into the writer
|
||||
@@ -166,12 +247,15 @@ class output_vector_sink
|
||||
void grow(const std::size_t length)
|
||||
{
|
||||
// use the capacity the vector already has, else at least double it;
|
||||
// never less than 64 characters, so that a small value does not
|
||||
// never less than min_vector_sink_capacity, so that a small value does not
|
||||
// reallocate for every few bytes
|
||||
const std::size_t needed = position + length;
|
||||
v->resize((std::max)(std::size_t{64}, needed <= v->capacity()
|
||||
const std::size_t written = position();
|
||||
const std::size_t needed = written + length;
|
||||
v->resize((std::max)(min_vector_sink_capacity, needed <= v->capacity()
|
||||
? v->capacity()
|
||||
: (std::max)(needed, v->capacity() * 2)));
|
||||
cursor = v->data() + written;
|
||||
limit = v->data() + v->size();
|
||||
}
|
||||
|
||||
/// append @a length (at least max_reserve_size) characters that do not fit
|
||||
@@ -181,20 +265,24 @@ class output_vector_sink
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
void append(const CharType* s, const std::size_t length)
|
||||
{
|
||||
v->resize(position);
|
||||
if (v->capacity() - position < length)
|
||||
const std::size_t written = position();
|
||||
v->resize(written);
|
||||
if (v->capacity() - written < length)
|
||||
{
|
||||
// grow geometrically, with room for the writes that follow, so
|
||||
// that the next grow() need not reallocate right away
|
||||
v->reserve((std::max)(v->capacity() * 2, position + length + max_reserve_size));
|
||||
v->reserve((std::max)(v->capacity() * 2, written + length + max_reserve_size));
|
||||
}
|
||||
v->insert(v->end(), s, s + length);
|
||||
position = v->size();
|
||||
cursor = v->data() + v->size();
|
||||
limit = cursor;
|
||||
}
|
||||
|
||||
std::vector<CharType, AllocatorType>* v;
|
||||
/// number of characters of *v written so far
|
||||
std::size_t position;
|
||||
/// the write position: in *v, at the number of characters written so far
|
||||
CharType* cursor;
|
||||
/// the end of *v
|
||||
CharType* limit;
|
||||
};
|
||||
|
||||
/// output adapter for byte vectors
|
||||
@@ -277,9 +365,17 @@ class output_string_adapter : public output_adapter_protocol<CharType>
|
||||
///
|
||||
/// Wraps the polymorphic output_adapter_t so the same binary_writer template can
|
||||
/// also target arbitrary adapters (output streams, strings, user-provided
|
||||
/// adapters) via the `output_adapter`-based overloads. Each write still goes
|
||||
/// through one virtual call, exactly as before; only the concrete sinks above
|
||||
/// avoid it.
|
||||
/// adapters) via the `output_adapter`-based overloads.
|
||||
///
|
||||
/// A write through the adapter is a virtual call, and for a stream a call with
|
||||
/// a sentry on top, which costs more than encoding the few bytes of most values.
|
||||
/// The sink therefore collects what is written in a buffer and passes it on in
|
||||
/// one piece when it is full, a write is too large for it, or @ref flush is
|
||||
/// called. Everything written reaches the adapter in the same order and with
|
||||
/// the same bytes; the destructor passes on what is left, so that a write that
|
||||
/// throws leaves the adapter with exactly the bytes written before it. Only
|
||||
/// @ref flush can report an error of the adapter (the destructor must not
|
||||
/// throw), so it has to be called once the value is written.
|
||||
template<typename CharType>
|
||||
class output_adapter_sink
|
||||
{
|
||||
@@ -290,36 +386,121 @@ class output_adapter_sink
|
||||
JSON_ASSERT(oa);
|
||||
}
|
||||
|
||||
output_adapter_sink(const output_adapter_sink&) = delete;
|
||||
output_adapter_sink& operator=(const output_adapter_sink&) = delete;
|
||||
output_adapter_sink& operator=(output_adapter_sink&&) = delete;
|
||||
|
||||
output_adapter_sink(output_adapter_sink&& other) noexcept
|
||||
: oa(std::move(other.oa)), used(other.used)
|
||||
{
|
||||
std::memcpy(buffer.data(), other.buffer.data(), other.used * sizeof(CharType));
|
||||
other.used = 0;
|
||||
}
|
||||
|
||||
~output_adapter_sink()
|
||||
{
|
||||
// a normal return has flushed already, so this is for a write that threw
|
||||
if (used != 0)
|
||||
{
|
||||
JSON_TRY
|
||||
{
|
||||
flush();
|
||||
}
|
||||
JSON_CATCH(...)
|
||||
{
|
||||
// nothing can be done about an adapter that throws here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
void write_character(CharType c)
|
||||
{
|
||||
oa->write_character(c);
|
||||
if (JSON_HEDLEY_UNLIKELY(used == buffer_size))
|
||||
{
|
||||
flush();
|
||||
}
|
||||
buffer[used++] = c;
|
||||
}
|
||||
|
||||
// no JSON_HEDLEY_NON_NULL: forwards (null, 0) for empty payloads, exactly as
|
||||
// the type-erased path already did before this sink existed
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
void write_characters(const CharType* s, std::size_t length)
|
||||
{
|
||||
oa->write_characters(s, length);
|
||||
if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length))
|
||||
{
|
||||
write_large(s, length);
|
||||
return;
|
||||
}
|
||||
if (length <= 32)
|
||||
{
|
||||
copy_short(buffer.data() + used, s, length, std::integral_constant<bool, sizeof(CharType) == 1>());
|
||||
used += length;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::memcpy(buffer.data() + used, s, length * sizeof(CharType));
|
||||
used += length;
|
||||
}
|
||||
}
|
||||
|
||||
/// space for writing up to @a length characters (see
|
||||
/// output_vector_sink::reserve)
|
||||
CharType* reserve(const std::size_t length) noexcept
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
CharType* reserve(const std::size_t length)
|
||||
{
|
||||
JSON_ASSERT(length <= max_reserve_size);
|
||||
static_cast<void>(length);
|
||||
return reserved.data();
|
||||
if (JSON_HEDLEY_UNLIKELY(buffer_size - used < length))
|
||||
{
|
||||
flush();
|
||||
}
|
||||
return buffer.data() + used;
|
||||
}
|
||||
|
||||
/// write the first @a length characters written to the space from reserve()
|
||||
void commit(const std::size_t length)
|
||||
/// keep the first @a length characters written to the space from reserve()
|
||||
JSON_HEDLEY_ALWAYS_INLINE
|
||||
void commit(const std::size_t length) noexcept
|
||||
{
|
||||
oa->write_characters(reserved.data(), length);
|
||||
used += length;
|
||||
}
|
||||
|
||||
/// pass everything written so far on to the adapter
|
||||
void flush()
|
||||
{
|
||||
if (used != 0)
|
||||
{
|
||||
// reset first: the characters count as written even if the adapter throws
|
||||
const std::size_t length = used;
|
||||
used = 0;
|
||||
oa->write_characters(buffer.data(), length);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
/// a write that does not fit the space left in the buffer
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
void write_large(const CharType* s, const std::size_t length)
|
||||
{
|
||||
flush();
|
||||
if (length >= buffer_size)
|
||||
{
|
||||
oa->write_characters(s, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::memcpy(buffer.data(), s, length * sizeof(CharType));
|
||||
used = length;
|
||||
}
|
||||
}
|
||||
|
||||
/// large enough for reserve() at any time
|
||||
static constexpr std::size_t buffer_size = 4 * max_reserve_size;
|
||||
|
||||
output_adapter_t<CharType> oa;
|
||||
std::array<CharType, max_reserve_size> reserved{};
|
||||
/// number of characters of @ref buffer not yet passed on
|
||||
std::size_t used = 0;
|
||||
std::array<CharType, buffer_size> buffer{};
|
||||
};
|
||||
|
||||
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -105,17 +105,13 @@ class output_buffer
|
||||
}
|
||||
|
||||
private:
|
||||
/// the size of a growth step (a function: std::min() takes a reference,
|
||||
/// which a static constexpr member does not have before C++17)
|
||||
static constexpr std::size_t step() noexcept
|
||||
{
|
||||
return 0x10000;
|
||||
}
|
||||
/// the size of a growth step
|
||||
static constexpr std::size_t step = 0x10000;
|
||||
|
||||
static StringType& sized(StringType& out, std::size_t estimate)
|
||||
{
|
||||
out.reserve(estimate);
|
||||
out.resize((std::min)((std::max)(estimate, static_cast<std::size_t>(64)), step()));
|
||||
out.resize(detail::clamp(estimate, std::size_t{64}, step));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -123,8 +119,9 @@ class output_buffer
|
||||
{
|
||||
const auto used = static_cast<std::size_t>(m_pos - m_out.data());
|
||||
// (a step does not go beyond the reserved estimate, so that a good
|
||||
// estimate is never copied to a larger allocation)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step(), m_out.capacity()), used + n + 256);
|
||||
// estimate is never copied to a larger allocation; not a clamp: the
|
||||
// lower bound used + n + 256 can exceed capacity(), and then it must win)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256);
|
||||
if (size > m_out.capacity())
|
||||
{
|
||||
m_out.reserve((std::max)(m_out.capacity() * 2, size));
|
||||
@@ -171,6 +168,34 @@ inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexc
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Whether dump() with ensure_ascii writes the string at s unchanged, for a
|
||||
/// string of the source without escape sequences (which has no quote,
|
||||
/// backslash, or control character): no byte is DEL (0x7F) or above. Adding 1
|
||||
/// to each byte sets its high bit exactly for 0x7F, and a carry into the next
|
||||
/// byte only comes from 0xFF, whose own high bit is set anyway.
|
||||
inline bool is_ascii_without_delete(const char* s, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(s + i);
|
||||
if (((v | (v + ones)) & high) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (static_cast<unsigned char>(s[i]) >= 0x7F)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// A stack that starts in a buffer of the caller (a local array) and moves to
|
||||
/// the heap (a vector of the caller) only when that is full, so that dumps of
|
||||
/// shallow documents need no allocation. The top is a pointer, as in
|
||||
@@ -286,15 +311,19 @@ class view_serializer
|
||||
|
||||
void dump(const node* root)
|
||||
{
|
||||
if (!m_style.pretty && !m_style.ensure_ascii)
|
||||
if (!m_style.pretty)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
if (m_style.ensure_ascii)
|
||||
{
|
||||
dump_compact<true>(root);
|
||||
dump_compact_ascii(root);
|
||||
}
|
||||
else if (m_style.source_numbers)
|
||||
{
|
||||
dump_compact<true, false>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_compact<false>(root);
|
||||
dump_compact<false, false>(root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -400,6 +429,41 @@ class view_serializer
|
||||
m_out.put('"');
|
||||
}
|
||||
|
||||
/// dump_compact() with ensure_ascii, out of line: the walk without it is
|
||||
/// faster when it is not merged with this one into dump()
|
||||
NLOHMANN_VIEW_NOINLINE void dump_compact_ascii(const node* root)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_compact<true, true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_compact<false, true>(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// whether dump_compact() copies a string or key from the source: one of
|
||||
/// the source, and with ensure_ascii one without bytes >= 0x7F (an overload
|
||||
/// each, so that the walk without ensure_ascii is compiled exactly as
|
||||
/// before: with GCC, a condition that only folds away later changed its
|
||||
/// block layout and made it 4-6% slower)
|
||||
static bool copied(const node& n, const char* /*src*/, std::false_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0;
|
||||
}
|
||||
|
||||
static bool copied(const node& n, const char* src, std::true_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len);
|
||||
}
|
||||
|
||||
/// a string with ensure_ascii (dump_compact()), out of line
|
||||
NLOHMANN_VIEW_NOINLINE void write_ascii_string(const node& n)
|
||||
{
|
||||
write_string(n);
|
||||
}
|
||||
|
||||
/// the copies of dump_compact() that are not fixed-size moves (long
|
||||
/// strings, or near the end of the source); out of line, so that the
|
||||
/// compiler does not merge the fixed-size moves into this call
|
||||
@@ -417,16 +481,18 @@ class view_serializer
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the compact output without ensure_ascii (the default dump())
|
||||
@brief the compact output (the default dump(), or with ensure_ascii)
|
||||
|
||||
The same walk as dump(), with the write position in a local variable
|
||||
(stores through char pointers would otherwise force a reload of the
|
||||
buffer's members after each one), and with strings and number tokens of
|
||||
the source copied by fixed-size moves of 32 bytes where the source has
|
||||
that many bytes left, instead of a library call per token. The buffer
|
||||
keeps 64 bytes of slack for the overshoot.
|
||||
keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a
|
||||
string of the source is copied only if it has no byte >= 0x7F (see
|
||||
is_ascii_without_delete()); any other string is written by write_string().
|
||||
*/
|
||||
template<bool SourceNumbers>
|
||||
template<bool SourceNumbers, bool Ascii>
|
||||
void dump_compact(const node* root)
|
||||
{
|
||||
struct frame
|
||||
@@ -478,7 +544,14 @@ class view_serializer
|
||||
const auto escaped = [&](const node & n)
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
write_decoded(n);
|
||||
if (enabled(Ascii))
|
||||
{
|
||||
write_ascii_string(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
write_decoded(n);
|
||||
}
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
};
|
||||
@@ -501,7 +574,7 @@ class view_serializer
|
||||
switch (static_cast<value_t>(n->kind))
|
||||
{
|
||||
case value_t::string:
|
||||
if ((n->flags & node_flags::storage) == 0)
|
||||
if (copied(*n, src, std::integral_constant<bool, Ascii>()))
|
||||
{
|
||||
room(n->len + 2);
|
||||
*w++ = '"';
|
||||
@@ -632,7 +705,7 @@ class view_serializer
|
||||
if (cur.object)
|
||||
{
|
||||
const node& key = *at;
|
||||
if ((key.flags & node_flags::storage) == 0)
|
||||
if (copied(key, src, std::integral_constant<bool, Ascii>()))
|
||||
{
|
||||
room(key.len + 3);
|
||||
*w++ = '"';
|
||||
@@ -794,8 +867,7 @@ class view_serializer
|
||||
static char* write_double_value_at(char* w, double x)
|
||||
{
|
||||
// (from the bits: without the checks of to_chars())
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &x, sizeof(bits));
|
||||
auto bits = bit_cast<std::uint64_t>(x);
|
||||
if (NLOHMANN_VIEW_UNLIKELY((bits & 0x7FF0000000000000u) == 0x7FF0000000000000u))
|
||||
{
|
||||
return write_text_at(w, "null", 4);
|
||||
@@ -843,9 +915,10 @@ class view_serializer
|
||||
{
|
||||
const char* const s = m_doc.str(n);
|
||||
m_out.put('"');
|
||||
if ((n.flags & node_flags::storage) == 0 && !m_style.ensure_ascii)
|
||||
if ((n.flags & node_flags::storage) == 0 && (!m_style.ensure_ascii || is_ascii_without_delete(s, n.len)))
|
||||
{
|
||||
// a string of the source without escape sequences has nothing to escape
|
||||
// a string of the source without escape sequences has nothing to
|
||||
// escape (with ensure_ascii, if it is printable ASCII)
|
||||
m_out.put(s, n.len);
|
||||
}
|
||||
else if (m_style.ensure_ascii)
|
||||
|
||||
@@ -50,7 +50,12 @@
|
||||
#endif
|
||||
#if NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && !defined(NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9))))
|
||||
// (GCC before 4.9 has no SSSE3 intrinsics without -mssse3)
|
||||
#include <cpuid.h>
|
||||
// <cpuid.h> of GCC before 11 has no include guard, and json.hpp and
|
||||
// json_view.hpp both include it
|
||||
#ifndef NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#define NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
#include <tmmintrin.h>
|
||||
#define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define NLOHMANN_VIEW_SSSE3_TARGET __attribute__((target("ssse3")))
|
||||
|
||||
+439
-66
@@ -19,6 +19,7 @@
|
||||
#define INCLUDE_NLOHMANN_JSON_HPP_
|
||||
|
||||
#include <algorithm> // all_of, find, for_each, none_of
|
||||
#include <array> // array
|
||||
#include <cmath> // isnan
|
||||
#include <cstddef> // nullptr_t, ptrdiff_t, size_t
|
||||
#include <cstdint> // uint8_t
|
||||
@@ -6321,7 +6322,7 @@ public:
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(detail::binary_reserve_hint(j));
|
||||
result.reserve(detail::binary_initial_capacity(j));
|
||||
vector_writer(result, error_handler).write_cbor(j);
|
||||
return result;
|
||||
}
|
||||
@@ -6331,7 +6332,9 @@ public:
|
||||
static void to_cbor(const basic_json& j, detail::output_adapter<std::uint8_t> o,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<std::uint8_t>(o, error_handler).write_cbor(j);
|
||||
binary_writer<std::uint8_t> writer(o, error_handler);
|
||||
writer.write_cbor(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a CBOR serialization of a given JSON value
|
||||
@@ -6339,7 +6342,9 @@ public:
|
||||
static void to_cbor(const basic_json& j, detail::output_adapter<char> o,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<char>(o, error_handler).write_cbor(j);
|
||||
binary_writer<char> writer(o, error_handler);
|
||||
writer.write_cbor(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a MessagePack serialization of a given JSON value
|
||||
@@ -6348,7 +6353,7 @@ public:
|
||||
const error_handler_t error_handler = error_handler_t::keep)
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(detail::binary_reserve_hint(j));
|
||||
result.reserve(detail::binary_initial_capacity(j));
|
||||
vector_writer(result, error_handler).write_msgpack(j);
|
||||
return result;
|
||||
}
|
||||
@@ -6358,7 +6363,9 @@ public:
|
||||
static void to_msgpack(const basic_json& j, detail::output_adapter<std::uint8_t> o,
|
||||
const error_handler_t error_handler = error_handler_t::keep)
|
||||
{
|
||||
binary_writer<std::uint8_t>(o, error_handler).write_msgpack(j);
|
||||
binary_writer<std::uint8_t> writer(o, error_handler);
|
||||
writer.write_msgpack(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a MessagePack serialization of a given JSON value
|
||||
@@ -6366,7 +6373,9 @@ public:
|
||||
static void to_msgpack(const basic_json& j, detail::output_adapter<char> o,
|
||||
const error_handler_t error_handler = error_handler_t::keep)
|
||||
{
|
||||
binary_writer<char>(o, error_handler).write_msgpack(j);
|
||||
binary_writer<char> writer(o, error_handler);
|
||||
writer.write_msgpack(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a UBJSON serialization of a given JSON value
|
||||
@@ -6377,7 +6386,7 @@ public:
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(detail::binary_reserve_hint(j));
|
||||
result.reserve(detail::binary_initial_capacity(j));
|
||||
vector_writer(result, error_handler).write_ubjson(j, use_size, use_type);
|
||||
return result;
|
||||
}
|
||||
@@ -6388,7 +6397,9 @@ public:
|
||||
const bool use_size = false, const bool use_type = false,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<std::uint8_t>(o, error_handler).write_ubjson(j, use_size, use_type);
|
||||
binary_writer<std::uint8_t> writer(o, error_handler);
|
||||
writer.write_ubjson(j, use_size, use_type);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a UBJSON serialization of a given JSON value
|
||||
@@ -6397,7 +6408,9 @@ public:
|
||||
const bool use_size = false, const bool use_type = false,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<char>(o, error_handler).write_ubjson(j, use_size, use_type);
|
||||
binary_writer<char> writer(o, error_handler);
|
||||
writer.write_ubjson(j, use_size, use_type);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a BJData serialization of a given JSON value
|
||||
@@ -6409,7 +6422,7 @@ public:
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(detail::binary_reserve_hint(j));
|
||||
result.reserve(detail::binary_initial_capacity(j));
|
||||
vector_writer(result, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
|
||||
return result;
|
||||
}
|
||||
@@ -6421,7 +6434,9 @@ public:
|
||||
const bjdata_version_t version = bjdata_version_t::draft2,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<std::uint8_t>(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
|
||||
binary_writer<std::uint8_t> writer(o, error_handler);
|
||||
writer.write_ubjson(j, use_size, use_type, true, true, version);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a BJData serialization of a given JSON value
|
||||
@@ -6431,7 +6446,9 @@ public:
|
||||
const bjdata_version_t version = bjdata_version_t::draft2,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<char>(o, error_handler).write_ubjson(j, use_size, use_type, true, true, version);
|
||||
binary_writer<char> writer(o, error_handler);
|
||||
writer.write_ubjson(j, use_size, use_type, true, true, version);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a BSON serialization of a given JSON value
|
||||
@@ -6440,7 +6457,7 @@ public:
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(detail::binary_reserve_hint(j));
|
||||
result.reserve(detail::binary_initial_capacity(j));
|
||||
vector_writer(result, error_handler).write_bson(j);
|
||||
return result;
|
||||
}
|
||||
@@ -6450,7 +6467,9 @@ public:
|
||||
static void to_bson(const basic_json& j, detail::output_adapter<std::uint8_t> o,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<std::uint8_t>(o, error_handler).write_bson(j);
|
||||
binary_writer<std::uint8_t> writer(o, error_handler);
|
||||
writer.write_bson(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a BSON serialization of a given JSON value
|
||||
@@ -6458,7 +6477,9 @@ public:
|
||||
static void to_bson(const basic_json& j, detail::output_adapter<char> o,
|
||||
const error_handler_t error_handler = detail::binary_writer_default_error_handler())
|
||||
{
|
||||
binary_writer<char>(o, error_handler).write_bson(j);
|
||||
binary_writer<char> writer(o, error_handler);
|
||||
writer.write_bson(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a BON8 serialization of a given JSON value
|
||||
@@ -6466,7 +6487,7 @@ public:
|
||||
static std::vector<std::uint8_t> to_bon8(const basic_json& j)
|
||||
{
|
||||
std::vector<std::uint8_t> result;
|
||||
result.reserve(detail::binary_reserve_hint(j));
|
||||
result.reserve(detail::binary_initial_capacity(j));
|
||||
vector_writer(result).write_bon8(j);
|
||||
return result;
|
||||
}
|
||||
@@ -6475,14 +6496,18 @@ public:
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/to_bon8/
|
||||
static void to_bon8(const basic_json& j, detail::output_adapter<std::uint8_t> o)
|
||||
{
|
||||
binary_writer<std::uint8_t>(o).write_bon8(j);
|
||||
binary_writer<std::uint8_t> writer(o);
|
||||
writer.write_bon8(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a BON8 serialization of a given JSON value
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/to_bon8/
|
||||
static void to_bon8(const basic_json& j, detail::output_adapter<char> o)
|
||||
{
|
||||
binary_writer<char>(o).write_bon8(j);
|
||||
binary_writer<char> writer(o);
|
||||
writer.write_bon8(j);
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/// @brief create a JSON value from an input in CBOR format
|
||||
@@ -6878,6 +6903,72 @@ public:
|
||||
/// @name JSON Patch functions
|
||||
/// @{
|
||||
|
||||
private:
|
||||
/// the members of an operation of a JSON patch that are of interest
|
||||
struct patch_members
|
||||
{
|
||||
basic_json* op = nullptr;
|
||||
basic_json* path = nullptr;
|
||||
basic_json* value = nullptr;
|
||||
basic_json* from = nullptr;
|
||||
};
|
||||
|
||||
/// @brief find the members "op", "path", "value", and "from" of the object @a val in one pass
|
||||
static patch_members find_patch_members(const basic_json& val, std::true_type /*key_type is string_t*/)
|
||||
{
|
||||
patch_members members;
|
||||
for (auto& member : *val.m_data.m_value.object)
|
||||
{
|
||||
const string_t& key = member.first;
|
||||
switch (key.size())
|
||||
{
|
||||
case 2:
|
||||
if (key == "op")
|
||||
{
|
||||
members.op = &member.second;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
if (key == "path")
|
||||
{
|
||||
members.path = &member.second;
|
||||
}
|
||||
else if (key == "from")
|
||||
{
|
||||
members.from = &member.second;
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
if (key == "value")
|
||||
{
|
||||
members.value = &member.second;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
/// @brief find the members by lookup for key types other than string_t
|
||||
static patch_members find_patch_members(const basic_json& val, std::false_type /*key_type is not string_t*/)
|
||||
{
|
||||
const auto find_member = [&val](const char* name) -> basic_json *
|
||||
{
|
||||
const auto it = val.m_data.m_value.object->find(string_t(name));
|
||||
return it != val.m_data.m_value.object->end() ? &it->second : nullptr;
|
||||
};
|
||||
|
||||
patch_members members;
|
||||
members.op = find_member("op");
|
||||
members.path = find_member("path");
|
||||
members.value = find_member("value");
|
||||
members.from = find_member("from");
|
||||
return members;
|
||||
}
|
||||
|
||||
public:
|
||||
/// @brief applies a JSON patch in-place without copying the object
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/patch/
|
||||
void patch_inplace(const basic_json& json_patch)
|
||||
@@ -7045,31 +7136,155 @@ public:
|
||||
detail::throw_patch_not_an_array(json_patch);
|
||||
}
|
||||
|
||||
// the usual patch operations "add", "remove", "replace" and "test" on
|
||||
// a pointer without escapes that denotes existing array elements and
|
||||
// object members (as "add" does for the parent of the new value), as
|
||||
// a diff creates them: apply the operation and return true; return
|
||||
// false, with nothing changed, for all others
|
||||
string_t token;
|
||||
typename json_pointer::template plain_steps<basic_json> steps;
|
||||
const auto try_plain = [&result, &token, &steps](patch_operations operation, const string_t& path, const patch_members & members, const basic_json & val)
|
||||
{
|
||||
if (path.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// a missing "value" is reported below
|
||||
const basic_json* const value_ptr = members.value;
|
||||
const basic_json& value = value_ptr != nullptr ? *value_ptr : val;
|
||||
if (operation != patch_operations::remove && value_ptr == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case patch_operations::replace:
|
||||
{
|
||||
basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps);
|
||||
if (target == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*target = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
case patch_operations::test:
|
||||
{
|
||||
const basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token, steps);
|
||||
// a failing test is reported below
|
||||
return target != nullptr && *target == value;
|
||||
}
|
||||
|
||||
case patch_operations::add:
|
||||
case patch_operations::remove:
|
||||
{
|
||||
// the parent of the pointer and its last reference token
|
||||
std::size_t slash = path.size();
|
||||
while (slash > 0 && path[slash - 1] != '/')
|
||||
{
|
||||
--slash;
|
||||
}
|
||||
if (slash == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
--slash;
|
||||
|
||||
basic_json* const parent = json_pointer::resolve_plain(&result, path, slash, token, steps);
|
||||
if (parent == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
token.clear();
|
||||
token.append(path.data() + slash + 1, path.size() - slash - 1);
|
||||
if (token.find_first_of('~') != string_t::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (operation == patch_operations::add)
|
||||
{
|
||||
if (parent->is_object())
|
||||
{
|
||||
(*parent)[token] = value;
|
||||
return true;
|
||||
}
|
||||
if (parent->is_array())
|
||||
{
|
||||
typename basic_json::size_type idx{};
|
||||
if (token == "-")
|
||||
{
|
||||
parent->push_back(value);
|
||||
return true;
|
||||
}
|
||||
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx <= parent->size())
|
||||
{
|
||||
parent->insert(parent->begin() + static_cast<difference_type>(idx), value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parent->is_object())
|
||||
{
|
||||
const auto it = parent->find(token);
|
||||
if (it == parent->end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
parent->erase(it);
|
||||
return true;
|
||||
}
|
||||
if (parent->is_array())
|
||||
{
|
||||
typename basic_json::size_type idx{};
|
||||
if (json_pointer::template parse_array_index<basic_json_t>(token, idx) == json_pointer::array_index_status::ok && idx < parent->size())
|
||||
{
|
||||
parent->erase(idx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case patch_operations::move:
|
||||
case patch_operations::copy:
|
||||
case patch_operations::invalid:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// the name of the first member to check
|
||||
const string_t op_member = "op";
|
||||
|
||||
// iterate and apply the operations
|
||||
for (const auto& val : json_patch)
|
||||
{
|
||||
// wrapper to get a value for an operation
|
||||
const auto get_value = [&val](const string_t& op,
|
||||
const string_t& member,
|
||||
const auto get_value = [&val](basic_json * found,
|
||||
const string_t& op,
|
||||
const char* member,
|
||||
bool string_type) -> basic_json &
|
||||
{
|
||||
// find value
|
||||
auto it = val.m_data.m_value.object->find(member);
|
||||
|
||||
// check if the desired value is present
|
||||
if (JSON_HEDLEY_UNLIKELY(it == val.m_data.m_value.object->end()))
|
||||
if (JSON_HEDLEY_UNLIKELY(found == nullptr))
|
||||
{
|
||||
detail::throw_patch_missing_member(op, member, false, val);
|
||||
detail::throw_patch_missing_member(op, string_t(member), false, val);
|
||||
}
|
||||
|
||||
// check if the result is of type string
|
||||
if (JSON_HEDLEY_UNLIKELY(string_type && !it->second.is_string()))
|
||||
if (JSON_HEDLEY_UNLIKELY(string_type && !found->is_string()))
|
||||
{
|
||||
detail::throw_patch_missing_member(op, member, true, val);
|
||||
detail::throw_patch_missing_member(op, string_t(member), true, val);
|
||||
}
|
||||
|
||||
// no error: return value
|
||||
return it->second;
|
||||
return *found;
|
||||
};
|
||||
|
||||
// type check: every element of the array must be an object
|
||||
@@ -7078,16 +7293,30 @@ public:
|
||||
detail::throw_patch_not_an_array(val);
|
||||
}
|
||||
|
||||
const patch_members members = find_patch_members(val, std::is_same<typename object_t::key_type, string_t> {});
|
||||
|
||||
// collect mandatory members
|
||||
const string_t& op = *get_value("op", "op", true).m_data.m_value.string;
|
||||
const string_t& path = *get_value(op, "path", true).m_data.m_value.string;
|
||||
const string_t& op = *get_value(members.op, op_member, "op", true).m_data.m_value.string;
|
||||
const string_t& path = *get_value(members.path, op, "path", true).m_data.m_value.string;
|
||||
const patch_operations operation = get_op(op);
|
||||
|
||||
// the usual operations on a plain pointer are applied without
|
||||
// building the pointer's reference tokens; anything else (and
|
||||
// every error) is handled below
|
||||
if (try_plain(operation, path, members, val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// the general operations may change any container
|
||||
steps.reset();
|
||||
json_pointer ptr(path);
|
||||
|
||||
switch (get_op(op))
|
||||
switch (operation)
|
||||
{
|
||||
case patch_operations::add:
|
||||
{
|
||||
operation_add(ptr, get_value("add", "value", false));
|
||||
operation_add(ptr, get_value(members.value, "add", "value", false));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7100,13 +7329,13 @@ public:
|
||||
case patch_operations::replace:
|
||||
{
|
||||
// the "path" location must exist - use at()
|
||||
result.at(ptr) = get_value("replace", "value", false);
|
||||
result.at(ptr) = get_value(members.value, "replace", "value", false);
|
||||
break;
|
||||
}
|
||||
|
||||
case patch_operations::move:
|
||||
{
|
||||
const string_t& from_path = *get_value("move", "from", true).m_data.m_value.string;
|
||||
const string_t& from_path = *get_value(members.from, "move", "from", true).m_data.m_value.string;
|
||||
json_pointer from_ptr(from_path);
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(is_proper_prefix(from_ptr, ptr)))
|
||||
@@ -7129,7 +7358,7 @@ public:
|
||||
|
||||
case patch_operations::copy:
|
||||
{
|
||||
const string_t& from_path = *get_value("copy", "from", true).m_data.m_value.string;
|
||||
const string_t& from_path = *get_value(members.from, "copy", "from", true).m_data.m_value.string;
|
||||
const json_pointer from_ptr(from_path);
|
||||
|
||||
// The copy is functionally identical to an "add"
|
||||
@@ -7147,7 +7376,7 @@ public:
|
||||
{
|
||||
// check if "value" matches the one at "path"
|
||||
// the "path" location must exist - use at()
|
||||
success = (result.at(ptr) == get_value("test", "value", false));
|
||||
success = (result.at(ptr) == get_value(members.value, "test", "value", false));
|
||||
}
|
||||
JSON_INTERNAL_CATCH (out_of_range&)
|
||||
{
|
||||
@@ -7191,7 +7420,8 @@ public:
|
||||
{
|
||||
// the patch
|
||||
basic_json result(value_t::array);
|
||||
diff_recursively(result, source, target, path, 0);
|
||||
string_t buffer = path;
|
||||
diff_recursively(result, source, target, buffer, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -7235,31 +7465,130 @@ public:
|
||||
// function it appears in. In diff_recursively, which is on the call stack
|
||||
// once per nesting level, that made every level cost kilobytes of stack.
|
||||
|
||||
/// @brief append an operation @a op for @a path to @a result, with @a value if it is not null
|
||||
static void diff_operation(basic_json& result, const char* op, const string_t& path, const basic_json* value)
|
||||
{
|
||||
// the keys are distinct and in the order of std::map; the members are
|
||||
// added without looking them up
|
||||
basic_json entry(value_t::object);
|
||||
auto& members = *entry.m_data.m_value.object;
|
||||
auto hint = members.end();
|
||||
json_pointer::add_flattened(members, hint, string_t("op"), string_t(op), detail::priority_tag<2> {});
|
||||
json_pointer::add_flattened(members, hint, string_t("path"), path, detail::priority_tag<2> {});
|
||||
if (value != nullptr)
|
||||
{
|
||||
json_pointer::add_flattened(members, hint, string_t("value"), *value, detail::priority_tag<2> {});
|
||||
}
|
||||
entry.set_parents();
|
||||
result.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
/// @brief append a "replace" operation for @a path with @a value to @a result
|
||||
static void diff_replace(basic_json& result, const string_t& path, const basic_json& value)
|
||||
{
|
||||
result.push_back(
|
||||
{
|
||||
{"op", "replace"}, {"path", path}, {"value", value}
|
||||
});
|
||||
diff_operation(result, "replace", path, &value);
|
||||
}
|
||||
|
||||
/// @brief append a "remove" operation for @a path to @a result
|
||||
static void diff_remove(basic_json& result, const string_t& path)
|
||||
{
|
||||
result.push_back(object(
|
||||
{
|
||||
{"op", "remove"}, {"path", path}
|
||||
}));
|
||||
diff_operation(result, "remove", path, nullptr);
|
||||
}
|
||||
|
||||
/// @brief append an "add" operation for @a path with @a value to @a result
|
||||
static void diff_add(basic_json& result, const string_t& path, const basic_json& value)
|
||||
{
|
||||
result.push_back(
|
||||
diff_operation(result, "add", path, &value);
|
||||
}
|
||||
|
||||
/// @brief the values in the target of the members of a source object, for the
|
||||
/// objects of up to 16 members without allocating
|
||||
///
|
||||
/// reserve() must be called with the number of values that are added.
|
||||
class diff_target_values
|
||||
{
|
||||
public:
|
||||
diff_target_values() noexcept
|
||||
: inline_items() // NOLINT(readability-redundant-member-init)
|
||||
{}
|
||||
diff_target_values(const diff_target_values&) = delete;
|
||||
diff_target_values& operator=(const diff_target_values&) = delete;
|
||||
diff_target_values(diff_target_values&&) = delete;
|
||||
diff_target_values& operator=(diff_target_values&&) = delete;
|
||||
~diff_target_values() = default;
|
||||
|
||||
void reserve(const std::size_t n)
|
||||
{
|
||||
{"op", "add"}, {"path", path}, {"value", value}
|
||||
});
|
||||
if (n > inline_size)
|
||||
{
|
||||
large.resize(n);
|
||||
items = large.data();
|
||||
}
|
||||
}
|
||||
|
||||
void push_back(const basic_json* value)
|
||||
{
|
||||
items[count++] = value;
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept
|
||||
{
|
||||
return count;
|
||||
}
|
||||
|
||||
const basic_json* operator[](const std::size_t i) const noexcept
|
||||
{
|
||||
return items[i];
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t inline_size = 16;
|
||||
/// the values of a small object, left uninitialized: they are written
|
||||
/// before they are read (the user-provided constructor keeps the
|
||||
/// member initialization, which -Weffc++ asks for, from zeroing them)
|
||||
struct uninitialized_items : std::array<const basic_json*, inline_size>
|
||||
{
|
||||
uninitialized_items() noexcept {} // NOLINT(modernize-use-equals-default,hicpp-use-equals-default,cppcoreguidelines-pro-type-member-init,hicpp-member-init): see above
|
||||
};
|
||||
|
||||
std::size_t count = 0;
|
||||
std::vector<const basic_json*> large{}; // NOLINT(readability-redundant-member-init)
|
||||
uninitialized_items inline_items;
|
||||
const basic_json** items = inline_items.data();
|
||||
};
|
||||
|
||||
/// @brief append "/" and the decimal representation of @a index to @a path
|
||||
static void diff_append_index(string_t& path, std::size_t index)
|
||||
{
|
||||
path.push_back('/');
|
||||
json_pointer::append_index(path, index);
|
||||
}
|
||||
|
||||
/// @brief append "/" and the escaped @a key to @a path
|
||||
static void diff_append_key(string_t& path, const string_t& key)
|
||||
{
|
||||
path.push_back('/');
|
||||
detail::escape_into(path, key);
|
||||
}
|
||||
|
||||
/// @brief the path of the member @a key of the value at @a path
|
||||
static string_t diff_key_path(const string_t& path, const string_t& key)
|
||||
{
|
||||
string_t result;
|
||||
result.reserve(path.size() + key.size() + 1);
|
||||
result.append(path.data(), path.size());
|
||||
diff_append_key(result, key);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// @brief the path of the element @a index of the value at @a path
|
||||
static string_t diff_index_path(const string_t& path, const std::size_t index)
|
||||
{
|
||||
string_t result;
|
||||
result.reserve(path.size() + 8);
|
||||
result.append(path.data(), path.size());
|
||||
diff_append_index(result, index);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// @brief append the "remove" operations for the elements of array
|
||||
@@ -7272,13 +7601,18 @@ public:
|
||||
// in that order avoids the quadratic reinsertion done before
|
||||
for (std::size_t j = source.size(); j > index; --j)
|
||||
{
|
||||
diff_remove(result, detail::concat<string_t>(path, '/', detail::to_string<string_t>(j - 1)));
|
||||
diff_remove(result, diff_index_path(path, j - 1));
|
||||
}
|
||||
|
||||
// add other remaining elements
|
||||
for (std::size_t i = source.size(); i < target.size(); ++i)
|
||||
if (source.size() < target.size())
|
||||
{
|
||||
diff_add(result, detail::concat<string_t>(path, "/-"), target[i]);
|
||||
string_t tail_path = path;
|
||||
tail_path.append("/-", 2);
|
||||
for (std::size_t i = source.size(); i < target.size(); ++i)
|
||||
{
|
||||
diff_add(result, tail_path, target[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7287,7 +7621,7 @@ public:
|
||||
{
|
||||
for (const auto& it : added)
|
||||
{
|
||||
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
|
||||
diff_add(result, diff_key_path(path, it.key()), it.value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7303,15 +7637,31 @@ public:
|
||||
operations that remove every member of @a source and add every member of
|
||||
@a target to @a result, and returns false.
|
||||
*/
|
||||
template<typename TargetValues>
|
||||
static bool diff_object_keys(basic_json& result, const basic_json& source, const basic_json& target,
|
||||
const string_t& path, std::vector<const basic_json*>& target_values,
|
||||
const string_t& path, TargetValues& target_values,
|
||||
std::vector<const_iterator>& added)
|
||||
{
|
||||
// first pass: one lookup per source key
|
||||
// Objects of the same size usually have the same keys, in the same
|
||||
// order: walk them together as long as that holds, which takes no lookups.
|
||||
target_values.reserve(source.size());
|
||||
std::size_t common = 0;
|
||||
for (auto it = source.cbegin(); it != source.cend(); ++it)
|
||||
bool together = source.size() == target.size();
|
||||
for (auto it = source.cbegin(), partner = target.cbegin(); it != source.cend(); ++it)
|
||||
{
|
||||
if (together)
|
||||
{
|
||||
if (it.key() == partner.key())
|
||||
{
|
||||
target_values.push_back(&partner.value());
|
||||
++common;
|
||||
++partner;
|
||||
continue;
|
||||
}
|
||||
together = false;
|
||||
}
|
||||
|
||||
const auto found = target.find(it.key());
|
||||
if (found != target.cend())
|
||||
{
|
||||
@@ -7324,6 +7674,12 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// the same keys in the same order
|
||||
if (together)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// second pass: find the keys only target has. An object type that
|
||||
// keeps its members in insertion order, such as the one backing
|
||||
// ordered_json, also needs the common keys to be in the same order
|
||||
@@ -7389,6 +7745,13 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// equal objects, whose members are in a different order, need no
|
||||
// operations
|
||||
if (source == target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// slow path: the common keys are in a different relative
|
||||
// order in source and target (only possible for a
|
||||
// reorderable object_t like ordered_map). Building a
|
||||
@@ -7405,7 +7768,7 @@ public:
|
||||
// it moves it to the end, fixing its position.
|
||||
for (auto it = source.cbegin(); it != source.cend(); ++it)
|
||||
{
|
||||
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key())));
|
||||
diff_remove(result, diff_key_path(path, it.key()));
|
||||
}
|
||||
|
||||
// add every key that is either common (just removed
|
||||
@@ -7414,7 +7777,7 @@ public:
|
||||
// target exactly
|
||||
for (auto it = target.cbegin(); it != target.cend(); ++it)
|
||||
{
|
||||
diff_add(result, detail::concat<string_t>(path, '/', detail::escape(it.key())), it.value());
|
||||
diff_add(result, diff_key_path(path, it.key()), it.value());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -7423,6 +7786,9 @@ public:
|
||||
@brief @ref diff, for values at nesting level @a depth, appending the
|
||||
operations to @a result
|
||||
|
||||
@a path is the path of the values; it is a buffer that is extended and
|
||||
shortened again below, and is the same when the function returns.
|
||||
|
||||
Diffing two arrays or objects calls this function again, once per nesting
|
||||
level, so values nested deeply enough used to exhaust the call stack and
|
||||
terminate the process. The descent is bounded here: once @ref
|
||||
@@ -7430,7 +7796,7 @@ public:
|
||||
diff_iteratively diffs what is left without the call stack.
|
||||
*/
|
||||
static void diff_recursively(basic_json& result, const basic_json& source, const basic_json& target,
|
||||
const string_t& path, const std::size_t depth)
|
||||
string_t& path, const std::size_t depth)
|
||||
{
|
||||
// if the values are the same, there is nothing to do
|
||||
if (source == target)
|
||||
@@ -7442,9 +7808,9 @@ public:
|
||||
}
|
||||
|
||||
/// @brief @ref diff_recursively for values known to be unequal; callers
|
||||
/// compare first so they build no path for equal ones
|
||||
/// compare first so they extend the path only for unequal ones
|
||||
static void diff_unequal(basic_json& result, const basic_json& source, const basic_json& target,
|
||||
const string_t& path, const std::size_t depth)
|
||||
string_t& path, const std::size_t depth)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit()))
|
||||
{
|
||||
@@ -7463,6 +7829,8 @@ public:
|
||||
{
|
||||
case value_t::array:
|
||||
{
|
||||
const std::size_t path_length = path.size();
|
||||
|
||||
// first pass: traverse common elements
|
||||
std::size_t i = 0;
|
||||
while (i < source.size() && i < target.size())
|
||||
@@ -7470,7 +7838,9 @@ public:
|
||||
// recursive call to compare array values at index i
|
||||
if (!(source[i] == target[i]))
|
||||
{
|
||||
diff_unequal(result, source[i], target[i], detail::concat<string_t>(path, '/', detail::to_string<string_t>(i)), depth + 1);
|
||||
diff_append_index(path, i);
|
||||
diff_unequal(result, source[i], target[i], path, depth + 1);
|
||||
path.resize(path_length);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
@@ -7483,7 +7853,7 @@ public:
|
||||
|
||||
case value_t::object:
|
||||
{
|
||||
std::vector<const basic_json*> target_values;
|
||||
diff_target_values target_values;
|
||||
std::vector<const_iterator> added;
|
||||
if (diff_object_keys(result, source, target, path, target_values, added))
|
||||
{
|
||||
@@ -7492,6 +7862,7 @@ public:
|
||||
// lookup is needed. Deleted keys are interleaved here
|
||||
// too, in source's original order, to match the
|
||||
// historical (pre-reordering-aware) output order.
|
||||
const std::size_t path_length = path.size();
|
||||
std::size_t i = 0;
|
||||
for (auto it = source.cbegin(); it != source.cend(); ++it, ++i)
|
||||
{
|
||||
@@ -7499,13 +7870,15 @@ public:
|
||||
{
|
||||
if (!(it.value() == *target_values[i]))
|
||||
{
|
||||
diff_unequal(result, it.value(), *target_values[i], detail::concat<string_t>(path, '/', detail::escape(it.key())), depth + 1);
|
||||
diff_append_key(path, it.key());
|
||||
diff_unequal(result, it.value(), *target_values[i], path, depth + 1);
|
||||
path.resize(path_length);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// found a key that is not in target -> remove it
|
||||
diff_remove(result, detail::concat<string_t>(path, '/', detail::escape(it.key())));
|
||||
diff_remove(result, diff_key_path(path, it.key()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7636,7 +8009,7 @@ public:
|
||||
if (i < source_array.size() && i < target_array.size())
|
||||
{
|
||||
++stack.back().index;
|
||||
detail::concat_into(current_path, '/', detail::to_string<string_t>(i));
|
||||
diff_append_index(current_path, i);
|
||||
enter(source_array[i], target_array[i]);
|
||||
if (stack.size() == depth)
|
||||
{
|
||||
@@ -7659,7 +8032,7 @@ public:
|
||||
if (target_value_ptr != nullptr)
|
||||
{
|
||||
const basic_json& target_value = *target_value_ptr;
|
||||
detail::concat_into(current_path, '/', detail::escape(it.key()));
|
||||
diff_append_key(current_path, it.key());
|
||||
enter(it.value(), target_value);
|
||||
if (stack.size() == depth)
|
||||
{
|
||||
@@ -7669,7 +8042,7 @@ public:
|
||||
else
|
||||
{
|
||||
// found a key that is not in target -> remove it
|
||||
diff_remove(result, detail::concat<string_t>(current_path, '/', detail::escape(it.key())));
|
||||
diff_remove(result, diff_key_path(current_path, it.key()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Generated
+1924
-594516
File diff suppressed because it is too large.
Load diff
Generated
+4868
-932
File diff suppressed because it is too large.
Load diff
Generated
+10
-3
@@ -139,6 +139,12 @@
|
||||
#define NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD _ns
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_NO_SIMD
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
#define NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD _vns
|
||||
#else
|
||||
@@ -156,9 +162,9 @@
|
||||
#endif
|
||||
|
||||
// Construct the namespace ABI tags component
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l)
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m) json_abi ## a ## b ## c ## d ## e ## f ## g ## h ## i ## j ## k ## l ## m
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c, d, e, f, g, h, i, j, k, l, m) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c, d, e, f, g, h, i, j, k, l, m)
|
||||
|
||||
#define NLOHMANN_JSON_ABI_TAGS \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||
@@ -172,6 +178,7 @@
|
||||
NLOHMANN_JSON_ABI_TAG_OBJECTS_FOR_ENUM_KEYED_MAPS, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_STREAM_BULK_SCAN, \
|
||||
NLOHMANN_JSON_ABI_TAG_DISABLE_UNLOCKED_FILE_IO, \
|
||||
NLOHMANN_JSON_ABI_TAG_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_NO_SIMD, \
|
||||
NLOHMANN_JSON_ABI_TAG_VIEW_USE_SSSE3)
|
||||
|
||||
|
||||
Generated
+102
-24
@@ -690,7 +690,12 @@ NLOHMANN_JSON_NAMESPACE_END
|
||||
#endif
|
||||
#if NLOHMANN_VIEW_SSE2 && !NLOHMANN_VIEW_SSSE3 && !defined(NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE) && ((defined(__clang__) && __clang_major__ >= 4) || (defined(__GNUC__) && !defined(__clang__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9))))
|
||||
// (GCC before 4.9 has no SSSE3 intrinsics without -mssse3)
|
||||
#include <cpuid.h>
|
||||
// <cpuid.h> of GCC before 11 has no include guard, and json.hpp and
|
||||
// json_view.hpp both include it
|
||||
#ifndef NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#define NLOHMANN_JSON_CPUID_INCLUDED
|
||||
#include <cpuid.h>
|
||||
#endif
|
||||
#include <tmmintrin.h>
|
||||
#define NLOHMANN_VIEW_SSSE3_DISPATCH 1 // NOLINT(cppcoreguidelines-macro-to-enum,modernize-macro-to-enum)
|
||||
#define NLOHMANN_VIEW_SSSE3_TARGET __attribute__((target("ssse3")))
|
||||
@@ -6166,17 +6171,13 @@ class output_buffer
|
||||
}
|
||||
|
||||
private:
|
||||
/// the size of a growth step (a function: std::min() takes a reference,
|
||||
/// which a static constexpr member does not have before C++17)
|
||||
static constexpr std::size_t step() noexcept
|
||||
{
|
||||
return 0x10000;
|
||||
}
|
||||
/// the size of a growth step
|
||||
static constexpr std::size_t step = 0x10000;
|
||||
|
||||
static StringType& sized(StringType& out, std::size_t estimate)
|
||||
{
|
||||
out.reserve(estimate);
|
||||
out.resize((std::min)((std::max)(estimate, static_cast<std::size_t>(64)), step()));
|
||||
out.resize(detail::clamp(estimate, std::size_t{64}, step));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -6184,8 +6185,9 @@ class output_buffer
|
||||
{
|
||||
const auto used = static_cast<std::size_t>(m_pos - m_out.data());
|
||||
// (a step does not go beyond the reserved estimate, so that a good
|
||||
// estimate is never copied to a larger allocation)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step(), m_out.capacity()), used + n + 256);
|
||||
// estimate is never copied to a larger allocation; not a clamp: the
|
||||
// lower bound used + n + 256 can exceed capacity(), and then it must win)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step, m_out.capacity()), used + n + 256);
|
||||
if (size > m_out.capacity())
|
||||
{
|
||||
m_out.reserve((std::max)(m_out.capacity() * 2, size));
|
||||
@@ -6232,6 +6234,34 @@ inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexc
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Whether dump() with ensure_ascii writes the string at s unchanged, for a
|
||||
/// string of the source without escape sequences (which has no quote,
|
||||
/// backslash, or control character): no byte is DEL (0x7F) or above. Adding 1
|
||||
/// to each byte sets its high bit exactly for 0x7F, and a carry into the next
|
||||
/// byte only comes from 0xFF, whose own high bit is set anyway.
|
||||
inline bool is_ascii_without_delete(const char* s, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(s + i);
|
||||
if (((v | (v + ones)) & high) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (static_cast<unsigned char>(s[i]) >= 0x7F)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// A stack that starts in a buffer of the caller (a local array) and moves to
|
||||
/// the heap (a vector of the caller) only when that is full, so that dumps of
|
||||
/// shallow documents need no allocation. The top is a pointer, as in
|
||||
@@ -6347,15 +6377,19 @@ class view_serializer
|
||||
|
||||
void dump(const node* root)
|
||||
{
|
||||
if (!m_style.pretty && !m_style.ensure_ascii)
|
||||
if (!m_style.pretty)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
if (m_style.ensure_ascii)
|
||||
{
|
||||
dump_compact<true>(root);
|
||||
dump_compact_ascii(root);
|
||||
}
|
||||
else if (m_style.source_numbers)
|
||||
{
|
||||
dump_compact<true, false>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_compact<false>(root);
|
||||
dump_compact<false, false>(root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -6461,6 +6495,41 @@ class view_serializer
|
||||
m_out.put('"');
|
||||
}
|
||||
|
||||
/// dump_compact() with ensure_ascii, out of line: the walk without it is
|
||||
/// faster when it is not merged with this one into dump()
|
||||
NLOHMANN_VIEW_NOINLINE void dump_compact_ascii(const node* root)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_compact<true, true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_compact<false, true>(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// whether dump_compact() copies a string or key from the source: one of
|
||||
/// the source, and with ensure_ascii one without bytes >= 0x7F (an overload
|
||||
/// each, so that the walk without ensure_ascii is compiled exactly as
|
||||
/// before: with GCC, a condition that only folds away later changed its
|
||||
/// block layout and made it 4-6% slower)
|
||||
static bool copied(const node& n, const char* /*src*/, std::false_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0;
|
||||
}
|
||||
|
||||
static bool copied(const node& n, const char* src, std::true_type /*ensure_ascii*/) noexcept
|
||||
{
|
||||
return (n.flags & node_flags::storage) == 0 && is_ascii_without_delete(src + n.off, n.len);
|
||||
}
|
||||
|
||||
/// a string with ensure_ascii (dump_compact()), out of line
|
||||
NLOHMANN_VIEW_NOINLINE void write_ascii_string(const node& n)
|
||||
{
|
||||
write_string(n);
|
||||
}
|
||||
|
||||
/// the copies of dump_compact() that are not fixed-size moves (long
|
||||
/// strings, or near the end of the source); out of line, so that the
|
||||
/// compiler does not merge the fixed-size moves into this call
|
||||
@@ -6478,16 +6547,18 @@ class view_serializer
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the compact output without ensure_ascii (the default dump())
|
||||
@brief the compact output (the default dump(), or with ensure_ascii)
|
||||
|
||||
The same walk as dump(), with the write position in a local variable
|
||||
(stores through char pointers would otherwise force a reload of the
|
||||
buffer's members after each one), and with strings and number tokens of
|
||||
the source copied by fixed-size moves of 32 bytes where the source has
|
||||
that many bytes left, instead of a library call per token. The buffer
|
||||
keeps 64 bytes of slack for the overshoot.
|
||||
keeps 64 bytes of slack for the overshoot. With ensure_ascii (Ascii), a
|
||||
string of the source is copied only if it has no byte >= 0x7F (see
|
||||
is_ascii_without_delete()); any other string is written by write_string().
|
||||
*/
|
||||
template<bool SourceNumbers>
|
||||
template<bool SourceNumbers, bool Ascii>
|
||||
void dump_compact(const node* root)
|
||||
{
|
||||
struct frame
|
||||
@@ -6539,7 +6610,14 @@ class view_serializer
|
||||
const auto escaped = [&](const node & n)
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
write_decoded(n);
|
||||
if (enabled(Ascii))
|
||||
{
|
||||
write_ascii_string(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
write_decoded(n);
|
||||
}
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
};
|
||||
@@ -6562,7 +6640,7 @@ class view_serializer
|
||||
switch (static_cast<value_t>(n->kind))
|
||||
{
|
||||
case value_t::string:
|
||||
if ((n->flags & node_flags::storage) == 0)
|
||||
if (copied(*n, src, std::integral_constant<bool, Ascii>()))
|
||||
{
|
||||
room(n->len + 2);
|
||||
*w++ = '"';
|
||||
@@ -6693,7 +6771,7 @@ class view_serializer
|
||||
if (cur.object)
|
||||
{
|
||||
const node& key = *at;
|
||||
if ((key.flags & node_flags::storage) == 0)
|
||||
if (copied(key, src, std::integral_constant<bool, Ascii>()))
|
||||
{
|
||||
room(key.len + 3);
|
||||
*w++ = '"';
|
||||
@@ -6855,8 +6933,7 @@ class view_serializer
|
||||
static char* write_double_value_at(char* w, double x)
|
||||
{
|
||||
// (from the bits: without the checks of to_chars())
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &x, sizeof(bits));
|
||||
auto bits = bit_cast<std::uint64_t>(x);
|
||||
if (NLOHMANN_VIEW_UNLIKELY((bits & 0x7FF0000000000000u) == 0x7FF0000000000000u))
|
||||
{
|
||||
return write_text_at(w, "null", 4);
|
||||
@@ -6904,9 +6981,10 @@ class view_serializer
|
||||
{
|
||||
const char* const s = m_doc.str(n);
|
||||
m_out.put('"');
|
||||
if ((n.flags & node_flags::storage) == 0 && !m_style.ensure_ascii)
|
||||
if ((n.flags & node_flags::storage) == 0 && (!m_style.ensure_ascii || is_ascii_without_delete(s, n.len)))
|
||||
{
|
||||
// a string of the source without escape sequences has nothing to escape
|
||||
// a string of the source without escape sequences has nothing to
|
||||
// escape (with ensure_ascii, if it is printable ASCII)
|
||||
m_out.put(s, n.len);
|
||||
}
|
||||
else if (m_style.ensure_ascii)
|
||||
|
||||
@@ -373,6 +373,25 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
|
||||
# the lexer again with the portable string scanning instead of SSE2/NEON, and on
|
||||
# x86-64 with SSSE3 compiled in (no check of the CPU)
|
||||
json_test_set_test_options(test-class_lexer_no_simd
|
||||
COMPILE_DEFINITIONS JSON_NO_SIMD
|
||||
)
|
||||
json_test_add_test_for(src/unit-class_lexer.cpp
|
||||
NAME test-class_lexer_no_simd
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$" AND NOT MSVC)
|
||||
json_test_set_test_options(test-class_lexer_ssse3
|
||||
COMPILE_OPTIONS -mssse3
|
||||
)
|
||||
json_test_add_test_for(src/unit-class_lexer.cpp
|
||||
NAME test-class_lexer_ssse3
|
||||
MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force}
|
||||
)
|
||||
endif()
|
||||
|
||||
# test reading from a std::istream again without scanning the get area in bulk
|
||||
json_test_set_test_options(test-stream-bulk-scan_disabled
|
||||
COMPILE_DEFINITIONS JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||
|
||||
@@ -24,6 +24,7 @@ foreach(test default noversion)
|
||||
JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON=1
|
||||
JSON_DISABLE_STREAM_BULK_SCAN=1
|
||||
JSON_DISABLE_UNLOCKED_FILE_IO=1
|
||||
JSON_NO_SIMD
|
||||
JSON_VIEW_NO_SIMD
|
||||
JSON_VIEW_USE_SSSE3)
|
||||
target_link_libraries(abi_config_${test}_all_tags PRIVATE abi_compat_main)
|
||||
|
||||
@@ -60,6 +60,10 @@ TEST_CASE("default namespace")
|
||||
expected += "_dufio";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
expected += "_ns";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
expected += "_vns";
|
||||
#endif
|
||||
|
||||
@@ -61,6 +61,10 @@ TEST_CASE("default namespace without version component")
|
||||
expected += "_dufio";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_NO_SIMD
|
||||
expected += "_ns";
|
||||
#endif
|
||||
|
||||
#ifdef JSON_VIEW_NO_SIMD
|
||||
expected += "_vns";
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,15 @@ Micro-benchmarks for parsing, serialization and the binary formats, written with
|
||||
| `ViewAccept` | validate with `json_document::accept`; compare with `Accept` |
|
||||
| `ViewMaterialize` | convert a parsed `json_document` into a `json` value |
|
||||
| `ViewDump` | serialize a parsed `json_document`; compare with `Dump` |
|
||||
| `SaxParse` | parse with a SAX handler that only counts events, so no values are built |
|
||||
| `Copy`, `CopyAssign`, `CopyRing` | copy a parsed document and destroy a copy, both timed: a temporary copy, an assignment over an earlier copy, and replacing the oldest of eight live copies |
|
||||
| `CopyOnly`, `DestroyOnly` | only the copy or only the destruction, the other half untimed; a fast destructor can leave work to the next allocations, so these tell the halves apart but do not compare versions |
|
||||
| `Equal`, `Iterate` | compare a parsed document with a copy, or visit every value with range-based `for` loops |
|
||||
| `Flatten`, `FlattenOrdered`, `Unflatten` | `flatten()` for `json` and `ordered_json`, and `unflatten()` |
|
||||
| `Diff`, `Patch` | `diff()` against the document with every eighth scalar changed, and applying that patch |
|
||||
| `JsonPointerParse`, `JsonPointerToString`, `JsonPointerAt`, `JsonPointerContains` | the JSON pointers of every scalar in a document: parse them from strings, turn them back into strings, look them up with `at()` and `contains()` |
|
||||
| `GetVectorInt`, `GetVectorDouble`, `GetMap` | `get<std::vector<std::int64_t>>()`, `get<std::vector<double>>()` and `get<std::map<std::string, json>>()` on large arrays and a large flat object |
|
||||
| `ToJsonStrings` | create a `json` array from a `std::vector` of 100,000 strings, copied or moved, and destroy it again |
|
||||
|
||||
The input files are those of [nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark) (`canada`,
|
||||
`citm_catalog`, `twitter`), a large `jeopardy` file, and number-heavy files (`floats`, `signed_ints`, ...).
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
build/
|
||||
@@ -0,0 +1,90 @@
|
||||
# json_view compared with other libraries
|
||||
|
||||
The in-tree benchmarks in [`tests/benchmarks`](../README.md) measure `json_document` against `json::parse` only. The
|
||||
programs here compare it with [yyjson](https://github.com/ibireme/yyjson),
|
||||
[simdjson](https://github.com/simdjson/simdjson), and [Boost.JSON](https://github.com/boostorg/json): the question
|
||||
users ask when they pick a library. They are not built by CMake or run by CI.
|
||||
|
||||
## Reproducing the numbers
|
||||
|
||||
`compare.py` builds the programs against `include/` of this checkout, runs them, and writes the results together with
|
||||
everything needed to reproduce them to `results/<date>-<host>.md` (and `.csv`): the date, the commit, the CPU, the
|
||||
OS, the compiler, the flags, and the versions of all libraries.
|
||||
|
||||
```sh
|
||||
python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory> [--native] [--rounds 30]
|
||||
```
|
||||
|
||||
- `--data` is the downloaded [test data](https://github.com/nlohmann/json_test_data), e.g. the `test_files` directory
|
||||
of a CMake build directory. It needs `nativejson-benchmark/{twitter,citm_catalog,canada}.json` and
|
||||
`jeopardy/jeopardy.json`.
|
||||
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
|
||||
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
|
||||
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
|
||||
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
|
||||
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
|
||||
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
|
||||
- Only the Python 3 standard library is used; a C++17 compiler is needed (`CXX` and `CC` are honored).
|
||||
|
||||
For numbers worth publishing, use a quiet machine (see [Getting stable numbers](../README.md#getting-stable-numbers)),
|
||||
the default 30 rounds or more, and `--native` only if the other libraries were built for the same CPU.
|
||||
|
||||
### On GitHub-hosted runners
|
||||
|
||||
The workflow [json_view benchmarks](../../../.github/workflows/json_view_benchmarks.yml) runs `compare.py --download`
|
||||
on demand: by hand (Actions → "json_view benchmarks" → "Run workflow"), on an x86-64 or AArch64 Ubuntu runner with GCC
|
||||
or Clang, or when a pull request gets the label `benchmark`, on both architectures with GCC. The results appear as the
|
||||
job summary and as an artifact. Shared runners are noisy, so these numbers show
|
||||
where `json_view` stands on another architecture; they are not meant for publication.
|
||||
|
||||
## What is measured
|
||||
|
||||
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
|
||||
JSON-RPC request (`rpc`):
|
||||
|
||||
| workload | what it does |
|
||||
|---|---|
|
||||
| parse | build and free a document |
|
||||
| traverse | parse, then visit every value, convert every number, touch every string and key |
|
||||
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
|
||||
| dump | serialize a parsed document (compact) |
|
||||
|
||||
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
|
||||
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
|
||||
yyjson with numbers read as raw text (`YYJSON_READ_NUMBER_AS_RAW`), without converting them.
|
||||
|
||||
`bench_edit.cpp` measures read-modify-write: parse, apply the same logical edits with each library's own API, and
|
||||
serialize (compact). Workloads: `patch` (a handful of edits at fixed places) and `update` (edits in every record).
|
||||
An editable `json_document` edits in place; yyjson copies its immutable document into a mutable one first
|
||||
(`yyjson_doc_mut_copy`); Boost.JSON and `json::parse` build mutable DOMs; simdjson cannot edit a document. All
|
||||
outputs are checked to describe the same value.
|
||||
|
||||
Before anything is timed, all engines must accept each document and agree on the traversal: the number of values, the
|
||||
bytes of all strings and keys, and the sum of all numbers. All engines run interleaved in every round, and the best
|
||||
round is reported, as time and as a factor of the `json_view` time (below 1 means faster than `json_view`). Each timed
|
||||
call follows an untimed call of the same engine: otherwise the engine after `json::parse` pays for the allocator
|
||||
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
|
||||
slower on citm_catalog traverse).
|
||||
|
||||
The engines do not all offer the same features, which the numbers should be read with:
|
||||
|
||||
| engine | document | random access | editable | notes |
|
||||
|---|---|---|---|---|
|
||||
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
|
||||
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
|
||||
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
|
||||
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
|
||||
| Boost.JSON | owning, mutable DOM | yes | yes | monotonic resource |
|
||||
| `json::parse` | owning, mutable DOM | yes | yes | |
|
||||
|
||||
Reusing memory matters as much as the parser. simdjson DOM reuses its parser, so it writes into memory it already
|
||||
touched; a fresh `json_view` document or yyjson document gets new memory for every parse. On Linux, glibc returns large
|
||||
blocks to the system when they are freed, so every fresh parse of a large document pays a page fault per 4 KiB page:
|
||||
on x86-64 Linux, a fresh `json_view` parse of jeopardy took about twice as long as a reused one. On macOS on Apple
|
||||
silicon, with 16 KiB pages, the difference is much smaller. Compare "json_view (reused)" with "simdjson DOM", and the
|
||||
fresh `json_view` with "simdjson DOM (fresh)" and yyjson.
|
||||
|
||||
## Published results
|
||||
|
||||
Results are only published with the file `compare.py` wrote, which names the machine and the versions; see
|
||||
`results/`. Numbers from one machine and compiler do not carry over to another: rerun the script.
|
||||
@@ -0,0 +1,348 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Corpus benchmark: the read-only workloads of bench_view.cpp on any list of
|
||||
// JSON files (for example the benchmark sets of simdjson and yyjson).
|
||||
//
|
||||
// ./bench_corpus [--rounds N] file...
|
||||
//
|
||||
// For every file, all engines must accept it and agree on a traversal (value
|
||||
// count, string bytes, sum of numbers) before anything is timed. Workloads:
|
||||
// parse (build and free a document), traverse (visit every value, convert
|
||||
// every number), dump (compact), and for json_view also dump with the source
|
||||
// number text, compared with yyjson writing numbers read as raw text
|
||||
// (YYJSON_READ_NUMBER_AS_RAW). Results go to bench_corpus.csv.
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#endif
|
||||
#include <simdjson.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_view;
|
||||
|
||||
static volatile double g_sink;
|
||||
|
||||
struct stats
|
||||
{
|
||||
double num = 0;
|
||||
std::size_t str = 0, nodes = 0;
|
||||
};
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
{
|
||||
case json::value_t::object:
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
st.str += it.key().size();
|
||||
walk(*it, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const json_view e : v)
|
||||
{
|
||||
walk(e, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case json::value_t::number_integer:
|
||||
st.num += static_cast<double>(v.get<std::int64_t>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += v.get<double>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(yyjson_val* v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (yyjson_get_type(v))
|
||||
{
|
||||
case YYJSON_TYPE_OBJ:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* k, * val;
|
||||
yyjson_obj_foreach(v, idx, max, k, val)
|
||||
{
|
||||
st.str += yyjson_get_len(k);
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_ARR:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* val;
|
||||
yyjson_arr_foreach(v, idx, max, val)
|
||||
{
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_STR:
|
||||
st.str += yyjson_get_len(v);
|
||||
break;
|
||||
case YYJSON_TYPE_NUM:
|
||||
st.num += yyjson_is_sint(v) ? static_cast<double>(yyjson_get_sint(v)) : yyjson_is_uint(v) ? static_cast<double>(yyjson_get_uint(v)) : yyjson_get_real(v);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(simdjson::dom::element e, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (e.type())
|
||||
{
|
||||
case simdjson::dom::element_type::OBJECT:
|
||||
for (auto f : simdjson::dom::object(e))
|
||||
{
|
||||
st.str += f.key.size();
|
||||
walk(f.value, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::ARRAY:
|
||||
for (auto c : simdjson::dom::array(e))
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::STRING:
|
||||
st.str += std::string_view(e).size();
|
||||
break;
|
||||
case simdjson::dom::element_type::INT64:
|
||||
st.num += static_cast<double>(int64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::UINT64:
|
||||
st.num += static_cast<double>(uint64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::DOUBLE:
|
||||
st.num += double(e);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
static void walk(const boost::json::value& v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.kind())
|
||||
{
|
||||
case boost::json::kind::object:
|
||||
for (const auto& kv : v.get_object())
|
||||
{
|
||||
st.str += kv.key().size();
|
||||
walk(kv.value(), st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::array:
|
||||
for (const auto& c : v.get_array())
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case boost::json::kind::int64:
|
||||
st.num += static_cast<double>(v.get_int64());
|
||||
break;
|
||||
case boost::json::kind::uint64:
|
||||
st.num += static_cast<double>(v.get_uint64());
|
||||
break;
|
||||
case boost::json::kind::double_:
|
||||
st.num += v.get_double();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static std::string slurp(const std::string& p)
|
||||
{
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
std::fprintf(stderr, "cannot open %s\n", p.c_str());
|
||||
std::exit(1);
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static bool same(const stats& a, const stats& b)
|
||||
{
|
||||
return a.nodes == b.nodes && a.str == b.str && (a.num == b.num || std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num));
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
int rounds = 0; // 0: by size
|
||||
std::vector<std::string> files;
|
||||
for (int i = 1; i < argc; ++i)
|
||||
{
|
||||
if (std::strcmp(argv[i], "--rounds") == 0 && i + 1 < argc)
|
||||
{
|
||||
// (clamped: 0 rounds by size, at most a million)
|
||||
rounds = static_cast<int>((std::min)((std::max)(std::strtol(argv[++i], nullptr, 10), 0L), 1000000L));
|
||||
}
|
||||
else
|
||||
{
|
||||
files.push_back(argv[i]);
|
||||
}
|
||||
}
|
||||
std::FILE* csv = std::fopen("bench_corpus.csv", "w");
|
||||
if (csv == nullptr)
|
||||
{
|
||||
std::perror("bench_corpus.csv");
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(csv, "file,bytes,workload,engine,ns\n");
|
||||
json_document reused;
|
||||
simdjson::dom::parser sj;
|
||||
for (const auto& path : files)
|
||||
{
|
||||
const std::string s = slurp(path);
|
||||
const std::string name = path.substr(path.rfind('/') + 1);
|
||||
const simdjson::padded_string ps(s);
|
||||
|
||||
// all engines must agree before timing
|
||||
stats a, b, c, d;
|
||||
const json_document doc = json_document::parse(s);
|
||||
walk(doc.root(), a);
|
||||
yyjson_doc* y = yyjson_read(s.data(), s.size(), 0);
|
||||
auto sjr = sj.parse(ps);
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
boost::json::parse_options opt;
|
||||
opt.numbers = boost::json::number_precision::precise;
|
||||
boost::json::monotonic_resource mr0;
|
||||
const boost::json::value bv = boost::json::parse(s, &mr0, opt);
|
||||
#endif
|
||||
if (y == nullptr || sjr.error())
|
||||
{
|
||||
std::printf("%-34s skipped (an engine rejects it)\n", name.c_str());
|
||||
yyjson_doc_free(y);
|
||||
continue;
|
||||
}
|
||||
walk(yyjson_doc_get_root(y), b);
|
||||
walk(sjr.value_unsafe(), c);
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
walk(bv, d);
|
||||
#else
|
||||
d = a;
|
||||
#endif
|
||||
yyjson_doc_free(y);
|
||||
const bool ok = same(a, b) && same(a, c) && same(a, d);
|
||||
|
||||
const int r = rounds > 0 ? rounds : static_cast<int>(std::max<std::size_t>(3, std::min<std::size_t>(60, 400000000 / (s.size() + 1))));
|
||||
struct engine
|
||||
{
|
||||
std::string name;
|
||||
std::function<void()> fn;
|
||||
};
|
||||
json_document vd = json_document::parse(s);
|
||||
yyjson_doc* yd = yyjson_read(s.data(), s.size(), 0);
|
||||
yyjson_doc* yd_raw = yyjson_read(s.data(), s.size(), YYJSON_READ_NUMBER_AS_RAW);
|
||||
simdjson::dom::parser sjd;
|
||||
const simdjson::dom::element se = sjd.parse(ps).value_unsafe();
|
||||
const std::vector<std::pair<std::string, std::vector<engine>>> workloads =
|
||||
{
|
||||
{
|
||||
"parse", {
|
||||
{"json_view", [&] { auto x = json_document::parse(s); g_sink = static_cast<double>(x.node_count()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||
{"yyjson", [&] { yyjson_doc* x = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(x)); yyjson_doc_free(x); }},
|
||||
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); g_sink = v.is_object(); }},
|
||||
#endif
|
||||
}
|
||||
},
|
||||
{
|
||||
"traverse", {
|
||||
{"json_view", [&] { auto x = json_document::parse(s); stats st; walk(x.root(), st); g_sink = st.num; }},
|
||||
{"yyjson", [&] { yyjson_doc* x = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(x), st); g_sink = st.num; yyjson_doc_free(x); }},
|
||||
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); stats st; walk(v, st); g_sink = st.num; }},
|
||||
#endif
|
||||
}
|
||||
},
|
||||
{
|
||||
"dump", {
|
||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json_view (source numbers)", [&] { std::string o = vd.root().dump(-1, ' ', false, json_view::number_format::source); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson (raw numbers)", [&] { std::size_t n = 0; char* o = yyjson_write(yd_raw, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
}
|
||||
},
|
||||
};
|
||||
std::printf("%-34s %9zu B%s\n", name.c_str(), s.size(), ok ? "" : " [ENGINES DISAGREE]");
|
||||
for (const auto& wl : workloads)
|
||||
{
|
||||
std::vector<double> best(wl.second.size(), 1e300);
|
||||
for (int i = 0; i < r; ++i)
|
||||
{
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
// an untimed call first: whatever the previous engine left to the allocator
|
||||
// (e.g. thousands of freed json nodes) is cleaned up here, not in the timing
|
||||
wl.second[k].fn();
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
wl.second[k].fn();
|
||||
best[k] = std::min(best[k], std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count());
|
||||
}
|
||||
}
|
||||
std::printf(" %-9s", wl.first.c_str());
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
std::printf(" %s %.2f GB/s (%.2fx)", wl.second[k].name.c_str(), static_cast<double>(s.size()) / best[k], best[k] / best[0]);
|
||||
std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", name.c_str(), s.size(), wl.first.c_str(), wl.second[k].name.c_str(), best[k]);
|
||||
}
|
||||
std::printf("\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
yyjson_doc_free(yd);
|
||||
yyjson_doc_free(yd_raw);
|
||||
}
|
||||
std::fclose(csv);
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Read-modify-write benchmark: parse a document, apply the same logical edits
|
||||
// with each library's own API, and serialize it (compact).
|
||||
//
|
||||
// json_view json_editable_document: edits in place, unchanged values stay in the index
|
||||
// yyjson yyjson_read + yyjson_doc_mut_copy (the way to edit a parsed document)
|
||||
// Boost.JSON parse into a mutable DOM (monotonic resource, precise numbers), serialize
|
||||
// json::parse nlohmann::json today
|
||||
// simdjson has no mutable document and is not part of this comparison.
|
||||
//
|
||||
// Workloads:
|
||||
// patch a handful of edits at fixed places (scalars, a new member, a new array element)
|
||||
// update edits in every record (twitter: 100 statuses, citm: 243 performances,
|
||||
// canada: 480 rings, jeopardy: 216,930 questions): set scalars, erase a
|
||||
// member, add a member (canada: replace the first point of every ring)
|
||||
//
|
||||
// Build: see README.md (same flags as bench_view.cpp).
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#endif
|
||||
#include <yyjson.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_editable_document;
|
||||
using nlohmann::json_editable_view;
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
namespace bj = boost::json;
|
||||
#endif
|
||||
|
||||
static volatile std::size_t g_sink;
|
||||
|
||||
// ---------------- json_view ----------------
|
||||
|
||||
static std::string edit_view(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
json_editable_document d = json_editable_document::parse(s);
|
||||
const json_editable_view r = d.root();
|
||||
if (name == "twitter")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::int64_t i = 0;
|
||||
for (const json_editable_view st : r["statuses"])
|
||||
{
|
||||
d.set(st, "retweet_count", i++);
|
||||
d.set(st, "favorited", true);
|
||||
d.set(st, "text", "redacted");
|
||||
d.erase(st, "entities");
|
||||
d.set(st, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r["search_metadata"], "count", 200);
|
||||
d.set(r["statuses"][0], "text", "patched");
|
||||
d.set(r["statuses"][0]["user"], "followers_count", 1);
|
||||
d.set(r["statuses"][99], "favorited", true);
|
||||
d.set(r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (const json_editable_view p : r["performances"])
|
||||
{
|
||||
d.set(p, "name", "performance");
|
||||
d.set(p, "start", p["start"].get<std::int64_t>() + 1);
|
||||
d.erase(p, "seatMapImage");
|
||||
d.set(p, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r["events"]["138586341"], "name", "patched");
|
||||
d.set(r["performances"][0], "start", 0);
|
||||
d.set(r["venueNames"], "PLEYEL_PLEYEL", "Salle");
|
||||
d.set(r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json_editable_view coords = r["features"][0]["geometry"]["coordinates"];
|
||||
if (update)
|
||||
{
|
||||
for (const json_editable_view ring : coords)
|
||||
{
|
||||
d.set(ring, 0, json::array({0.5, 0.5}));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r["features"][0]["properties"], "name", "patched");
|
||||
d.set(r, "type", "FeatureCollection2");
|
||||
d.set(coords[0], 0, json::array({0.0, 0.0}));
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (const json_editable_view q : r)
|
||||
{
|
||||
d.set(q, "value", "$1");
|
||||
d.erase(q, "air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
d.set(r[0], "value", "$0");
|
||||
d.set(r[100000], "answer", "patched");
|
||||
d.set(r[216929], "round", "x");
|
||||
d.push_back(r, json::object({{"category", "NEW"}, {"value", "$5"}}));
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
d.set(r, "retweet_count", 1);
|
||||
d.set(r["user"], "name", "x");
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
d.set(r, "id", 4);
|
||||
d.set(r["params"], "subtrahend", 24);
|
||||
}
|
||||
return r.dump();
|
||||
}
|
||||
|
||||
// ---------------- nlohmann::json ----------------
|
||||
|
||||
static std::string edit_json(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
json r = json::parse(s);
|
||||
if (name == "twitter")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::int64_t i = 0;
|
||||
for (auto& st : r["statuses"])
|
||||
{
|
||||
st["retweet_count"] = i++;
|
||||
st["favorited"] = true;
|
||||
st["text"] = "redacted";
|
||||
st.erase("entities");
|
||||
st["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r["search_metadata"]["count"] = 200;
|
||||
r["statuses"][0]["text"] = "patched";
|
||||
r["statuses"][0]["user"]["followers_count"] = 1;
|
||||
r["statuses"][99]["favorited"] = true;
|
||||
r["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (auto& p : r["performances"])
|
||||
{
|
||||
p["name"] = "performance";
|
||||
p["start"] = p["start"].get<std::int64_t>() + 1;
|
||||
p.erase("seatMapImage");
|
||||
p["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r["events"]["138586341"]["name"] = "patched";
|
||||
r["performances"][0]["start"] = 0;
|
||||
r["venueNames"]["PLEYEL_PLEYEL"] = "Salle";
|
||||
r["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
json& coords = r["features"][0]["geometry"]["coordinates"];
|
||||
if (update)
|
||||
{
|
||||
for (auto& ring : coords)
|
||||
{
|
||||
ring[0] = json::array({0.5, 0.5});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r["features"][0]["properties"]["name"] = "patched";
|
||||
r["type"] = "FeatureCollection2";
|
||||
coords[0][0] = json::array({0.0, 0.0});
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (auto& q : r)
|
||||
{
|
||||
q["value"] = "$1";
|
||||
q.erase("air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
r[0]["value"] = "$0";
|
||||
r[100000]["answer"] = "patched";
|
||||
r[216929]["round"] = "x";
|
||||
r.push_back(json::object({{"category", "NEW"}, {"value", "$5"}}));
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
r["retweet_count"] = 1;
|
||||
r["user"]["name"] = "x";
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
r["id"] = 4;
|
||||
r["params"]["subtrahend"] = 24;
|
||||
}
|
||||
return r.dump();
|
||||
}
|
||||
|
||||
// ---------------- yyjson ----------------
|
||||
|
||||
static std::string edit_yyjson(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
yyjson_doc* idoc = yyjson_read(s.data(), s.size(), 0);
|
||||
yyjson_mut_doc* d = yyjson_doc_mut_copy(idoc, nullptr);
|
||||
yyjson_doc_free(idoc);
|
||||
yyjson_mut_val* r = yyjson_mut_doc_get_root(d);
|
||||
auto get = [](yyjson_mut_val * o, const char* k)
|
||||
{
|
||||
return yyjson_mut_obj_get(o, k);
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
yyjson_mut_val* sts = get(r, "statuses");
|
||||
if (update)
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* st;
|
||||
std::int64_t i = 0;
|
||||
yyjson_mut_arr_foreach(sts, idx, max, st)
|
||||
{
|
||||
yyjson_mut_set_sint(get(st, "retweet_count"), i++);
|
||||
yyjson_mut_set_bool(get(st, "favorited"), true);
|
||||
yyjson_mut_set_str(get(st, "text"), "redacted");
|
||||
yyjson_mut_obj_remove_key(st, "entities");
|
||||
yyjson_mut_obj_add_bool(d, st, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yyjson_mut_set_sint(get(get(r, "search_metadata"), "count"), 200);
|
||||
yyjson_mut_val* s0 = yyjson_mut_arr_get(sts, 0);
|
||||
yyjson_mut_set_str(get(s0, "text"), "patched");
|
||||
yyjson_mut_set_sint(get(get(s0, "user"), "followers_count"), 1);
|
||||
yyjson_mut_set_bool(get(yyjson_mut_arr_get(sts, 99), "favorited"), true);
|
||||
yyjson_mut_obj_add_bool(d, r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* p;
|
||||
yyjson_mut_arr_foreach(get(r, "performances"), idx, max, p)
|
||||
{
|
||||
yyjson_mut_set_str(get(p, "name"), "performance");
|
||||
yyjson_mut_val* start = get(p, "start");
|
||||
yyjson_mut_set_sint(start, yyjson_mut_get_sint(start) + 1);
|
||||
yyjson_mut_obj_remove_key(p, "seatMapImage");
|
||||
yyjson_mut_obj_add_bool(d, p, "edited", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yyjson_mut_set_str(get(get(get(r, "events"), "138586341"), "name"), "patched");
|
||||
yyjson_mut_set_sint(get(yyjson_mut_arr_get(get(r, "performances"), 0), "start"), 0);
|
||||
yyjson_mut_set_str(get(get(r, "venueNames"), "PLEYEL_PLEYEL"), "Salle");
|
||||
yyjson_mut_obj_add_bool(d, r, "patched", true);
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
yyjson_mut_val* f0 = yyjson_mut_arr_get(get(r, "features"), 0);
|
||||
yyjson_mut_val* coords = get(get(f0, "geometry"), "coordinates");
|
||||
if (update)
|
||||
{
|
||||
static const double half[2] = {0.5, 0.5};
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* ring;
|
||||
yyjson_mut_arr_foreach(coords, idx, max, ring)
|
||||
{
|
||||
yyjson_mut_arr_replace(ring, 0, yyjson_mut_arr_with_real(d, half, 2));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
static const double zero[2] = {0.0, 0.0};
|
||||
yyjson_mut_set_str(get(get(f0, "properties"), "name"), "patched");
|
||||
yyjson_mut_set_str(get(r, "type"), "FeatureCollection2");
|
||||
yyjson_mut_arr_replace(yyjson_mut_arr_get(coords, 0), 0, yyjson_mut_arr_with_real(d, zero, 2));
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_mut_val* q;
|
||||
yyjson_mut_arr_foreach(r, idx, max, q)
|
||||
{
|
||||
yyjson_mut_set_str(get(q, "value"), "$1");
|
||||
yyjson_mut_obj_remove_key(q, "air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 0), "value"), "$0");
|
||||
yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 100000), "answer"), "patched");
|
||||
yyjson_mut_set_str(get(yyjson_mut_arr_get(r, 216929), "round"), "x");
|
||||
yyjson_mut_val* o = yyjson_mut_obj(d);
|
||||
yyjson_mut_obj_add_str(d, o, "category", "NEW");
|
||||
yyjson_mut_obj_add_str(d, o, "value", "$5");
|
||||
yyjson_mut_arr_append(r, o);
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
yyjson_mut_set_sint(get(r, "retweet_count"), 1);
|
||||
yyjson_mut_set_str(get(get(r, "user"), "name"), "x");
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
yyjson_mut_set_sint(get(r, "id"), 4);
|
||||
yyjson_mut_set_sint(get(get(r, "params"), "subtrahend"), 24);
|
||||
}
|
||||
std::size_t n = 0;
|
||||
char* out = yyjson_mut_write(d, 0, &n);
|
||||
std::string result(out, n);
|
||||
std::free(out);
|
||||
yyjson_mut_doc_free(d);
|
||||
return result;
|
||||
}
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
// ---------------- Boost.JSON ----------------
|
||||
|
||||
static std::string edit_boost(const std::string& name, const std::string& s, bool update)
|
||||
{
|
||||
bj::monotonic_resource mr;
|
||||
bj::parse_options opt;
|
||||
opt.numbers = bj::number_precision::precise; // correctly rounded, like the others
|
||||
bj::value v = bj::parse(s, &mr, opt);
|
||||
bj::object* const obj = v.if_object(); // nullptr for jeopardy (an array)
|
||||
if (name == "twitter")
|
||||
{
|
||||
bj::array& sts = (*obj)["statuses"].as_array();
|
||||
if (update)
|
||||
{
|
||||
std::int64_t i = 0;
|
||||
for (auto& e : sts)
|
||||
{
|
||||
bj::object& st = e.as_object();
|
||||
st["retweet_count"] = i++;
|
||||
st["favorited"] = true;
|
||||
st["text"] = "redacted";
|
||||
st.erase("entities");
|
||||
st["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
(*obj)["search_metadata"].as_object()["count"] = 200;
|
||||
bj::object& s0 = sts[0].as_object();
|
||||
s0["text"] = "patched";
|
||||
s0["user"].as_object()["followers_count"] = 1;
|
||||
sts[99].as_object()["favorited"] = true;
|
||||
(*obj)["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
if (update)
|
||||
{
|
||||
for (auto& e : (*obj)["performances"].as_array())
|
||||
{
|
||||
bj::object& p = e.as_object();
|
||||
p["name"] = "performance";
|
||||
p["start"] = p["start"].as_int64() + 1;
|
||||
p.erase("seatMapImage");
|
||||
p["edited"] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
(*obj)["events"].as_object()["138586341"].as_object()["name"] = "patched";
|
||||
(*obj)["performances"].as_array()[0].as_object()["start"] = 0;
|
||||
(*obj)["venueNames"].as_object()["PLEYEL_PLEYEL"] = "Salle";
|
||||
(*obj)["patched"] = true;
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
bj::object& f0 = (*obj)["features"].as_array()[0].as_object();
|
||||
bj::array& coords = f0["geometry"].as_object()["coordinates"].as_array();
|
||||
if (update)
|
||||
{
|
||||
for (auto& ring : coords)
|
||||
{
|
||||
ring.as_array()[0] = bj::array({0.5, 0.5});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
f0["properties"].as_object()["name"] = "patched";
|
||||
(*obj)["type"] = "FeatureCollection2";
|
||||
coords[0].as_array()[0] = bj::array({0.0, 0.0});
|
||||
}
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
bj::array& a = v.as_array();
|
||||
if (update)
|
||||
{
|
||||
for (auto& e : a)
|
||||
{
|
||||
bj::object& q = e.as_object();
|
||||
q["value"] = "$1";
|
||||
q.erase("air_date");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
a[0].as_object()["value"] = "$0";
|
||||
a[100000].as_object()["answer"] = "patched";
|
||||
a[216929].as_object()["round"] = "x";
|
||||
a.push_back(bj::object({{"category", "NEW"}, {"value", "$5"}}));
|
||||
}
|
||||
}
|
||||
else if (name == "status")
|
||||
{
|
||||
(*obj)["retweet_count"] = 1;
|
||||
(*obj)["user"].as_object()["name"] = "x";
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
(*obj)["id"] = 4;
|
||||
(*obj)["params"].as_object()["subtrahend"] = 24;
|
||||
}
|
||||
return bj::serialize(v);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------- harness ----------------
|
||||
|
||||
static std::string slurp(const std::string& p)
|
||||
{
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
std::fprintf(stderr, "cannot open %s\n", p.c_str());
|
||||
std::exit(1);
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
std::fprintf(stderr, "usage: %s <json_test_data directory> [rounds] [document]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
const std::string T = std::string(argv[1]) + "/";
|
||||
// (clamped to 1 to a million)
|
||||
const int rounds = argc > 2 ? static_cast<int>((std::min)((std::max)(std::strtol(argv[2], nullptr, 10), 1L), 1000000L)) : 20;
|
||||
const std::string only = argc > 3 ? argv[3] : "";
|
||||
struct doc
|
||||
{
|
||||
std::string name, text;
|
||||
int batch;
|
||||
};
|
||||
std::vector<doc> docs;
|
||||
for (const char* f :
|
||||
{"nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json"
|
||||
})
|
||||
{
|
||||
std::string n = std::string(f).substr(std::string(f).find('/') + 1);
|
||||
docs.push_back({n.substr(0, n.size() - 5), slurp(T + f), 1});
|
||||
}
|
||||
docs.push_back({"status", json::parse(docs[0].text)["statuses"][0].dump(), 200});
|
||||
docs.push_back({"rpc", R"({"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 3})", 5000});
|
||||
|
||||
using fn = std::string (*)(const std::string&, const std::string&, bool);
|
||||
const std::vector<std::pair<std::string, fn>> engines =
|
||||
{
|
||||
{"json_view", edit_view}, {"yyjson", edit_yyjson},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", edit_boost},
|
||||
#endif
|
||||
{"json::parse", edit_json}
|
||||
};
|
||||
|
||||
std::FILE* csv = std::fopen("bench_edit.csv", "w");
|
||||
if (csv == nullptr)
|
||||
{
|
||||
std::perror("bench_edit.csv");
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(csv, "doc,bytes,workload,engine,ns\n");
|
||||
for (const auto& dc : docs)
|
||||
{
|
||||
if (!only.empty() && dc.name != only)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const bool update :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
if (update && (dc.name == "status" || dc.name == "rpc"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// all engines must produce the same value
|
||||
const json expected = json::parse(edit_json(dc.name, dc.text, update));
|
||||
bool ok = true;
|
||||
for (const auto& e : engines)
|
||||
{
|
||||
ok = ok && json::parse(e.second(dc.name, dc.text, update)) == expected;
|
||||
}
|
||||
std::vector<double> best(engines.size(), 1e300);
|
||||
const int r = dc.text.size() > 10000000 ? std::max(3, rounds / 4) : rounds;
|
||||
for (int i = 0; i < r; ++i)
|
||||
{
|
||||
for (std::size_t k = 0; k < engines.size(); ++k)
|
||||
{
|
||||
// an untimed call first: whatever the previous engine left to the allocator
|
||||
// (e.g. thousands of freed json nodes) is cleaned up here, not in the timing
|
||||
g_sink = engines[k].second(dc.name, dc.text, update).size();
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
for (int b = 0; b < dc.batch; ++b)
|
||||
{
|
||||
g_sink = engines[k].second(dc.name, dc.text, update).size();
|
||||
}
|
||||
const double ns = std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count() / dc.batch;
|
||||
best[k] = std::min(best[k], ns);
|
||||
}
|
||||
}
|
||||
const char* wl = update ? "update" : "patch";
|
||||
std::printf("%-13s %-7s %s", dc.name.c_str(), wl, ok ? "" : "[OUTPUT MISMATCH] ");
|
||||
for (std::size_t k = 0; k < engines.size(); ++k)
|
||||
{
|
||||
const double us = best[k] / 1e3;
|
||||
std::printf(" %s %.*fus (%.2fx)", engines[k].first.c_str(), us < 10 ? 3 : (us < 1000 ? 1 : 0), us, best[k] / best[0]);
|
||||
std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", dc.name.c_str(), dc.text.size(), wl, engines[k].first.c_str(), best[k]);
|
||||
}
|
||||
std::printf("\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
std::fclose(csv);
|
||||
}
|
||||
@@ -0,0 +1,755 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Same-feature-set benchmark: read-only JSON documents with random access.
|
||||
//
|
||||
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
|
||||
// yyjson yyjson_read(): immutable document, random access
|
||||
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
|
||||
// per parse): immutable, random access
|
||||
// references (different feature sets):
|
||||
// simdjson OD On-Demand: forward-only, lazy
|
||||
// Boost.JSON owning, mutable DOM (monotonic resource)
|
||||
// json::parse owning, mutable DOM (nlohmann today)
|
||||
//
|
||||
// Workloads: parse (build + free), traverse (visit everything, convert every
|
||||
// number, touch every string and key), select (a few fields per document),
|
||||
// dump (compact serialization of the parsed document).
|
||||
// All engines run interleaved in every round, each timed call after an untimed
|
||||
// one of the same engine; the best round is reported.
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#endif
|
||||
#include <simdjson.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::json_view;
|
||||
|
||||
static volatile double g_sink;
|
||||
|
||||
struct stats
|
||||
{
|
||||
double num = 0;
|
||||
std::size_t str = 0, nodes = 0;
|
||||
};
|
||||
|
||||
// ---------------- traversal ----------------
|
||||
|
||||
static void walk(json_view v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
{
|
||||
case json::value_t::object:
|
||||
for (auto it = v.begin(); it != v.end(); ++it)
|
||||
{
|
||||
st.str += it.key().size();
|
||||
walk(*it, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const json_view e : v)
|
||||
{
|
||||
walk(e, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case json::value_t::number_integer:
|
||||
st.num += static_cast<double>(v.get<std::int64_t>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(v.get<std::uint64_t>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += v.get<double>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(const json& j, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (j.type())
|
||||
{
|
||||
case json::value_t::object:
|
||||
for (const auto& kv : j.get_ref<const json::object_t&>())
|
||||
{
|
||||
st.str += kv.first.size();
|
||||
walk(kv.second, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::array:
|
||||
for (const auto& e : j.get_ref<const json::array_t&>())
|
||||
{
|
||||
walk(e, st);
|
||||
}
|
||||
break;
|
||||
case json::value_t::string:
|
||||
st.str += j.get_ref<const std::string&>().size();
|
||||
break;
|
||||
case json::value_t::number_integer:
|
||||
st.num += static_cast<double>(*j.get_ptr<const json::number_integer_t*>());
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
st.num += static_cast<double>(*j.get_ptr<const json::number_unsigned_t*>());
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
st.num += *j.get_ptr<const json::number_float_t*>();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(yyjson_val* v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (yyjson_get_type(v))
|
||||
{
|
||||
case YYJSON_TYPE_OBJ:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* k, * val;
|
||||
yyjson_obj_foreach(v, idx, max, k, val)
|
||||
{
|
||||
st.str += yyjson_get_len(k);
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_ARR:
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* val;
|
||||
yyjson_arr_foreach(v, idx, max, val)
|
||||
{
|
||||
walk(val, st);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case YYJSON_TYPE_STR:
|
||||
st.str += yyjson_get_len(v);
|
||||
break;
|
||||
case YYJSON_TYPE_NUM:
|
||||
if (yyjson_is_sint(v))
|
||||
{
|
||||
st.num += static_cast<double>(yyjson_get_sint(v));
|
||||
}
|
||||
else if (yyjson_is_uint(v))
|
||||
{
|
||||
st.num += static_cast<double>(yyjson_get_uint(v));
|
||||
}
|
||||
else
|
||||
{
|
||||
st.num += yyjson_get_real(v);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk(simdjson::dom::element e, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (e.type())
|
||||
{
|
||||
case simdjson::dom::element_type::OBJECT:
|
||||
for (auto f : simdjson::dom::object(e))
|
||||
{
|
||||
st.str += f.key.size();
|
||||
walk(f.value, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::ARRAY:
|
||||
for (auto c : simdjson::dom::array(e))
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case simdjson::dom::element_type::STRING:
|
||||
st.str += std::string_view(e).size();
|
||||
break;
|
||||
case simdjson::dom::element_type::INT64:
|
||||
st.num += static_cast<double>(int64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::UINT64:
|
||||
st.num += static_cast<double>(uint64_t(e));
|
||||
break;
|
||||
case simdjson::dom::element_type::DOUBLE:
|
||||
st.num += double(e);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void walk_od(simdjson::ondemand::value v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.type())
|
||||
{
|
||||
case simdjson::ondemand::json_type::object:
|
||||
for (auto f : v.get_object())
|
||||
{
|
||||
st.str += std::string_view(f.unescaped_key()).size();
|
||||
walk_od(f.value(), st);
|
||||
}
|
||||
break;
|
||||
case simdjson::ondemand::json_type::array:
|
||||
for (auto c : v.get_array())
|
||||
{
|
||||
walk_od(c.value(), st);
|
||||
}
|
||||
break;
|
||||
case simdjson::ondemand::json_type::string:
|
||||
st.str += std::string_view(v.get_string()).size();
|
||||
break;
|
||||
case simdjson::ondemand::json_type::number:
|
||||
{
|
||||
simdjson::ondemand::number n = v.get_number();
|
||||
switch (n.get_number_type())
|
||||
{
|
||||
case simdjson::ondemand::number_type::signed_integer:
|
||||
st.num += static_cast<double>(n.get_int64());
|
||||
break;
|
||||
case simdjson::ondemand::number_type::unsigned_integer:
|
||||
st.num += static_cast<double>(n.get_uint64());
|
||||
break;
|
||||
default:
|
||||
st.num += n.get_double();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case simdjson::ondemand::json_type::boolean:
|
||||
(void)bool(v.get_bool());
|
||||
break;
|
||||
default:
|
||||
(void)v.is_null();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
static void walk(const boost::json::value& v, stats& st)
|
||||
{
|
||||
++st.nodes;
|
||||
switch (v.kind())
|
||||
{
|
||||
case boost::json::kind::object:
|
||||
for (const auto& kv : v.get_object())
|
||||
{
|
||||
st.str += kv.key().size();
|
||||
walk(kv.value(), st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::array:
|
||||
for (const auto& c : v.get_array())
|
||||
{
|
||||
walk(c, st);
|
||||
}
|
||||
break;
|
||||
case boost::json::kind::string:
|
||||
st.str += v.get_string().size();
|
||||
break;
|
||||
case boost::json::kind::int64:
|
||||
st.num += static_cast<double>(v.get_int64());
|
||||
break;
|
||||
case boost::json::kind::uint64:
|
||||
st.num += static_cast<double>(v.get_uint64());
|
||||
break;
|
||||
case boost::json::kind::double_:
|
||||
st.num += v.get_double();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------- selective access ----------------
|
||||
// twitter: per status id, user.screen_name, retweet_count
|
||||
// citm: per performance id, eventId, #seatCategories; #events
|
||||
// canada: type, features[0].geometry.type, #coordinates
|
||||
// jeopardy: per question: round == "Final Jeopardy!", len(category)
|
||||
// status (one tweet): id, user.screen_name, retweet_count
|
||||
// rpc: method, params.minuend, id
|
||||
|
||||
static double pick(const std::string& name, json_view r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](json_view s)
|
||||
{
|
||||
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
|
||||
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (const json_view s : r["statuses"])
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (const json_view p : r["performances"])
|
||||
{
|
||||
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||
}
|
||||
acc += static_cast<double>(r["events"].size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json_view g = r["features"][0]["geometry"];
|
||||
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (const json_view q : r)
|
||||
{
|
||||
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(q["category"].get_string().size());
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(r["method"].get_string().size());
|
||||
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, const json& r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](const json & s)
|
||||
{
|
||||
acc += static_cast<double>(s["id"].get<std::uint64_t>());
|
||||
acc += static_cast<double>(s["user"]["screen_name"].get_ref<const std::string&>().size());
|
||||
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (const auto& s : r["statuses"])
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (const auto& p : r["performances"])
|
||||
{
|
||||
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
|
||||
}
|
||||
acc += static_cast<double>(r["events"].size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
const json& g = r["features"][0]["geometry"];
|
||||
acc += static_cast<double>(r["type"].get_ref<const std::string&>().size() + g["type"].get_ref<const std::string&>().size() + g["coordinates"].size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (const auto& q : r)
|
||||
{
|
||||
acc += q["round"].get_ref<const std::string&>() == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(q["category"].get_ref<const std::string&>().size());
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(r["method"].get_ref<const std::string&>().size());
|
||||
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, yyjson_val* r)
|
||||
{
|
||||
double acc = 0;
|
||||
auto get = [](yyjson_val * o, const char* k)
|
||||
{
|
||||
return yyjson_obj_get(o, k);
|
||||
};
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](yyjson_val * s)
|
||||
{
|
||||
acc += static_cast<double>(yyjson_get_uint(get(s, "id")));
|
||||
acc += static_cast<double>(yyjson_get_len(get(get(s, "user"), "screen_name")));
|
||||
acc += static_cast<double>(yyjson_get_sint(get(s, "retweet_count")));
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* s;
|
||||
yyjson_arr_foreach(get(r, "statuses"), idx, max, s)
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* p;
|
||||
yyjson_arr_foreach(get(r, "performances"), idx, max, p)
|
||||
{
|
||||
acc += static_cast<double>(yyjson_get_uint(get(p, "id")) + yyjson_get_uint(get(p, "eventId")) + yyjson_arr_size(get(p, "seatCategories")));
|
||||
}
|
||||
acc += static_cast<double>(yyjson_obj_size(get(r, "events")));
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
yyjson_val* g = get(yyjson_arr_get(get(r, "features"), 0), "geometry");
|
||||
acc += static_cast<double>(yyjson_get_len(get(r, "type")) + yyjson_get_len(get(g, "type")) + yyjson_arr_size(get(g, "coordinates")));
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
std::size_t idx, max;
|
||||
yyjson_val* q;
|
||||
yyjson_arr_foreach(r, idx, max, q)
|
||||
{
|
||||
acc += yyjson_equals_str(get(q, "round"), "Final Jeopardy!") ? 1 : 0;
|
||||
acc += static_cast<double>(yyjson_get_len(get(q, "category")));
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(yyjson_get_len(get(r, "method")));
|
||||
acc += static_cast<double>(yyjson_get_sint(get(get(r, "params"), "minuend")) + yyjson_get_sint(get(r, "id")));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick(const std::string& name, simdjson::dom::element r)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](simdjson::dom::element s)
|
||||
{
|
||||
acc += static_cast<double>(uint64_t(s["id"]));
|
||||
acc += static_cast<double>(std::string_view(s["user"]["screen_name"]).size());
|
||||
acc += static_cast<double>(int64_t(s["retweet_count"]));
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (auto s : simdjson::dom::array(r["statuses"]))
|
||||
{
|
||||
one(s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(r);
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
for (auto p : simdjson::dom::array(r["performances"]))
|
||||
{
|
||||
acc += static_cast<double>(uint64_t(p["id"]) + uint64_t(p["eventId"]) + simdjson::dom::array(p["seatCategories"]).size());
|
||||
}
|
||||
acc += static_cast<double>(simdjson::dom::object(r["events"]).size());
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
auto g = r["features"].at(0)["geometry"];
|
||||
acc += static_cast<double>(std::string_view(r["type"]).size() + std::string_view(g["type"]).size() + simdjson::dom::array(g["coordinates"]).size());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (auto q : simdjson::dom::array(r))
|
||||
{
|
||||
acc += std::string_view(q["round"]) == "Final Jeopardy!" ? 1 : 0;
|
||||
acc += static_cast<double>(std::string_view(q["category"]).size());
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(std::string_view(r["method"]).size());
|
||||
acc += static_cast<double>(int64_t(r["params"]["minuend"]) + int64_t(r["id"]));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
static double pick_od(const std::string& name, simdjson::ondemand::document& d)
|
||||
{
|
||||
double acc = 0;
|
||||
if (name == "twitter" || name == "status")
|
||||
{
|
||||
auto one = [&](simdjson::ondemand::object s)
|
||||
{
|
||||
acc += static_cast<double>(uint64_t(s["id"]));
|
||||
acc += static_cast<double>(std::string_view(s["user"]["screen_name"]).size());
|
||||
acc += static_cast<double>(int64_t(s["retweet_count"]));
|
||||
};
|
||||
if (name == "twitter")
|
||||
{
|
||||
for (auto s : d["statuses"])
|
||||
{
|
||||
one(s.get_object());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
one(d.get_object());
|
||||
}
|
||||
}
|
||||
else if (name == "citm_catalog")
|
||||
{
|
||||
simdjson::ondemand::object ev = d["events"].get_object();
|
||||
acc += static_cast<double>(ev.count_fields());
|
||||
for (auto p : d["performances"])
|
||||
{
|
||||
simdjson::ondemand::object o = p.get_object();
|
||||
const auto a = uint64_t(o["eventId"]) + uint64_t(o["id"]);
|
||||
simdjson::ondemand::array sc = o["seatCategories"].get_array();
|
||||
acc += static_cast<double>(a + sc.count_elements());
|
||||
}
|
||||
}
|
||||
else if (name == "canada")
|
||||
{
|
||||
acc += static_cast<double>(std::string_view(d["type"]).size());
|
||||
auto g = d["features"].at(0)["geometry"];
|
||||
acc += static_cast<double>(std::string_view(g["type"]).size());
|
||||
simdjson::ondemand::array co = g["coordinates"].get_array();
|
||||
acc += static_cast<double>(co.count_elements());
|
||||
}
|
||||
else if (name == "jeopardy")
|
||||
{
|
||||
for (auto q : d)
|
||||
{
|
||||
simdjson::ondemand::object o = q.get_object();
|
||||
acc += static_cast<double>(std::string_view(o["category"]).size());
|
||||
acc += std::string_view(o["round"]) == "Final Jeopardy!" ? 1 : 0;
|
||||
}
|
||||
}
|
||||
else if (name == "rpc")
|
||||
{
|
||||
acc += static_cast<double>(std::string_view(d["method"]).size());
|
||||
acc += static_cast<double>(int64_t(d["params"]["minuend"]));
|
||||
acc += static_cast<double>(int64_t(d["id"]));
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// ---------------- harness ----------------
|
||||
|
||||
static std::string slurp(const std::string& p)
|
||||
{
|
||||
std::ifstream f(p, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
std::fprintf(stderr, "cannot open %s\n", p.c_str());
|
||||
std::exit(1);
|
||||
}
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
struct engine
|
||||
{
|
||||
std::string name;
|
||||
std::function<void()> fn;
|
||||
};
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
std::fprintf(stderr, "usage: %s <json_test_data directory> [rounds] [document]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
const std::string T = std::string(argv[1]) + "/";
|
||||
// (clamped to 1 to a million)
|
||||
const int rounds = argc > 2 ? static_cast<int>((std::min)((std::max)(std::strtol(argv[2], nullptr, 10), 1L), 1000000L)) : 30;
|
||||
const std::string only = argc > 3 ? argv[3] : "";
|
||||
struct doc
|
||||
{
|
||||
std::string name, text;
|
||||
int batch;
|
||||
};
|
||||
std::vector<doc> docs;
|
||||
for (const char* f :
|
||||
{"nativejson-benchmark/twitter.json", "nativejson-benchmark/citm_catalog.json", "nativejson-benchmark/canada.json", "jeopardy/jeopardy.json"
|
||||
})
|
||||
{
|
||||
std::string n = std::string(f).substr(std::string(f).find('/') + 1);
|
||||
docs.push_back({n.substr(0, n.size() - 5), slurp(T + f), 1});
|
||||
}
|
||||
docs.push_back({"status", json::parse(docs[0].text)["statuses"][0].dump(), 200});
|
||||
docs.push_back({"rpc", R"({"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 3})", 5000});
|
||||
|
||||
// correctness cross-check of the workloads
|
||||
for (const auto& dc : docs)
|
||||
{
|
||||
stats a, b, c, dd;
|
||||
walk(json::parse(dc.text), a);
|
||||
auto d = json_document::parse(dc.text);
|
||||
walk(d.root(), b);
|
||||
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
|
||||
walk(yyjson_doc_get_root(y), c);
|
||||
simdjson::dom::parser p;
|
||||
walk(p.parse(dc.text).value(), dd);
|
||||
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
|
||||
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
|
||||
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
|
||||
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
|
||||
yyjson_doc_free(y);
|
||||
}
|
||||
|
||||
std::FILE* csv = std::fopen("bench_view.csv", "w");
|
||||
if (csv == nullptr)
|
||||
{
|
||||
std::perror("bench_view.csv");
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(csv, "doc,bytes,workload,engine,ns\n");
|
||||
json_document reused;
|
||||
simdjson::dom::parser sj;
|
||||
simdjson::ondemand::parser od;
|
||||
for (const auto& dc : docs)
|
||||
{
|
||||
if (!only.empty() && dc.name != only)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::string& s = dc.text;
|
||||
const simdjson::padded_string ps(s);
|
||||
const std::string name = dc.name;
|
||||
std::vector<std::pair<std::string, std::vector<engine>>> workloads;
|
||||
|
||||
workloads.push_back({"parse", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); g_sink = v.is_object(); }},
|
||||
#endif
|
||||
{"json::parse", [&] { json j = json::parse(s); g_sink = static_cast<double>(j.size()); }},
|
||||
}});
|
||||
workloads.push_back({"traverse", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
|
||||
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
|
||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
|
||||
#if JSON_VIEW_BENCH_BOOST
|
||||
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); stats st; walk(v, st); g_sink = st.num; }},
|
||||
#endif
|
||||
{"json::parse", [&] { json j = json::parse(s); stats st; walk(j, st); g_sink = st.num; }},
|
||||
}});
|
||||
workloads.push_back({"select", {
|
||||
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
|
||||
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
|
||||
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
|
||||
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
|
||||
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
|
||||
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
|
||||
}});
|
||||
{
|
||||
// serialization of an already parsed document
|
||||
static json_document vd;
|
||||
vd.read(s);
|
||||
static yyjson_doc* yd = nullptr;
|
||||
if (yd)
|
||||
{
|
||||
yyjson_doc_free(yd);
|
||||
}
|
||||
yd = yyjson_read(s.data(), s.size(), 0);
|
||||
static simdjson::dom::parser sjd;
|
||||
static simdjson::dom::element se;
|
||||
se = sjd.parse(ps).value_unsafe();
|
||||
static json jd;
|
||||
jd = json::parse(s);
|
||||
workloads.push_back({"dump", {
|
||||
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
|
||||
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
|
||||
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
|
||||
}});
|
||||
}
|
||||
|
||||
for (auto& wl : workloads)
|
||||
{
|
||||
std::vector<double> best(wl.second.size(), 1e300);
|
||||
const int r = s.size() > 10000000 ? std::max(3, rounds / 5) : rounds;
|
||||
for (int i = 0; i < r; ++i)
|
||||
{
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
// an untimed call first: whatever the previous engine left to the allocator
|
||||
// (e.g. thousands of freed json nodes) is cleaned up here, not in the timing
|
||||
wl.second[k].fn();
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
for (int b = 0; b < dc.batch; ++b)
|
||||
{
|
||||
wl.second[k].fn();
|
||||
}
|
||||
const double ns = std::chrono::duration<double, std::nano>(std::chrono::steady_clock::now() - t0).count() / dc.batch;
|
||||
best[k] = std::min(best[k], ns);
|
||||
}
|
||||
}
|
||||
const double ref = best[0];
|
||||
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
|
||||
for (std::size_t k = 0; k < wl.second.size(); ++k)
|
||||
{
|
||||
const double us = best[k] / 1e3;
|
||||
std::printf(" %s %s%s (%.2fx)", wl.second[k].name.c_str(), us >= 100 ? "" : "", (us >= 1000 ? std::to_string(static_cast<long>(us)) + "us" : (std::to_string(us).substr(0, 5) + "us")).c_str(), best[k] / ref);
|
||||
std::fprintf(csv, "%s,%zu,%s,%s,%.1f\n", dc.name.c_str(), s.size(), wl.first.c_str(), wl.second[k].name.c_str(), best[k]);
|
||||
}
|
||||
std::printf("\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
std::fclose(csv);
|
||||
}
|
||||
Executable
+342
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
# __ _____ _____ _____
|
||||
# __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
# | | |__ | | | | | | version 3.12.0
|
||||
# |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""Compare json_view with yyjson, simdjson, Boost.JSON, and json::parse.
|
||||
|
||||
Builds bench_view.cpp, bench_corpus.cpp, and bench_edit.cpp against the include/ directory of
|
||||
this checkout, runs them, and writes the results with everything needed to
|
||||
reproduce them (date, commit, CPU, OS, compiler, library versions, flags) to
|
||||
results/<date>-<host>.md and .csv next to this script.
|
||||
|
||||
The other libraries come from the system (--system, the default: pkg-config
|
||||
or Homebrew) or are downloaded as pinned releases and checked against their
|
||||
SHA-256 (--download). Boost.JSON is optional: without Boost headers, its
|
||||
columns are skipped, and the results say so.
|
||||
|
||||
Only the Python 3 standard library is used; a C++17 compiler is needed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
# runs only the compilers and benchmark binaries this script builds
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO = os.path.abspath(os.path.join(HERE, '..', '..', '..'))
|
||||
|
||||
# seconds without data after which a download fails
|
||||
DOWNLOAD_TIMEOUT = 60
|
||||
|
||||
# pinned releases for --download; the hashes are those of the archives
|
||||
#
|
||||
# The yyjson and simdjson archives are the tag archives GitHub generates
|
||||
# (archive/refs/tags/...), whose bytes GitHub does not guarantee to be stable. A SHA-256
|
||||
# mismatch for one of them means that GitHub regenerated the archive: check the upstream
|
||||
# tag, then pin the new hash.
|
||||
PINNED = {
|
||||
'yyjson': {
|
||||
'version': '0.13.0',
|
||||
'url': 'https://github.com/ibireme/yyjson/archive/refs/tags/0.13.0.tar.gz',
|
||||
'sha256': '34e0f62a2bc11ab20d601e8ca1cc2b2079503aa45119a19133d89d19b94a0fae',
|
||||
'dir': 'yyjson-0.13.0',
|
||||
},
|
||||
'simdjson': {
|
||||
'version': '4.6.11',
|
||||
'url': 'https://github.com/simdjson/simdjson/archive/refs/tags/v4.6.11.tar.gz',
|
||||
'sha256': '61d948fc24f0d793829ad658058e7597d064988a89b4607ea02e401a82df98ff',
|
||||
'dir': 'simdjson-4.6.11',
|
||||
},
|
||||
'boost': {
|
||||
'version': '1.92.0',
|
||||
'url': 'https://archives.boost.io/release/1.92.0/source/boost_1_92_0.tar.gz',
|
||||
'sha256': 'c4a3b310ddd2472416e091067166b0713be97c63f38c212c484ada022fd296ce',
|
||||
'dir': 'boost_1_92_0',
|
||||
},
|
||||
}
|
||||
|
||||
# the documents of bench_view.cpp, relative to the json_test_data directory
|
||||
DEFAULT_CORPUS = [
|
||||
'nativejson-benchmark/twitter.json',
|
||||
'nativejson-benchmark/citm_catalog.json',
|
||||
'nativejson-benchmark/canada.json',
|
||||
'jeopardy/jeopardy.json',
|
||||
]
|
||||
|
||||
|
||||
def run(cmd, **kwargs):
|
||||
print('+ ' + ' '.join(shlex.quote(c) for c in cmd), flush=True)
|
||||
# cmd is an argument list built by this script, never a shell string
|
||||
return subprocess.run(cmd, check=True, **kwargs) # nosec B603
|
||||
|
||||
|
||||
def output(cmd):
|
||||
try:
|
||||
# cmd is an argument list built by this script, never a shell string
|
||||
return subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip() # nosec B603
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# libraries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Library:
|
||||
"""include directories, sources to compile, and linker flags of a library"""
|
||||
|
||||
def __init__(self, name, include=None, sources=None, link=None, version=''):
|
||||
self.name = name
|
||||
self.include = include or []
|
||||
self.sources = sources or []
|
||||
self.link = link or []
|
||||
self.version = version
|
||||
|
||||
|
||||
def header_version(path, pattern):
|
||||
try:
|
||||
with open(path, encoding='utf-8', errors='replace') as f:
|
||||
m = re.search(pattern, f.read())
|
||||
return m.group(1) if m else ''
|
||||
except OSError:
|
||||
return ''
|
||||
|
||||
|
||||
def library_version(name, include_dirs):
|
||||
patterns = {
|
||||
'yyjson': ('yyjson.h', r'#define\s+YYJSON_VERSION_STRING\s+"([^"]+)"'),
|
||||
'simdjson': ('simdjson.h', r'#define\s+SIMDJSON_VERSION\s+"?([0-9.]+)"?'),
|
||||
'boost': (os.path.join('boost', 'version.hpp'), r'#define\s+BOOST_LIB_VERSION\s+"([^"]+)"'),
|
||||
}
|
||||
header, pattern = patterns[name]
|
||||
for d in include_dirs:
|
||||
v = header_version(os.path.join(d, header), pattern)
|
||||
if v:
|
||||
return v.replace('_', '.')
|
||||
return ''
|
||||
|
||||
|
||||
def system_library(name):
|
||||
"""a library found with pkg-config or Homebrew, or None"""
|
||||
flags = output(['pkg-config', '--cflags', '--libs', name]).split()
|
||||
if flags:
|
||||
include = [f[2:] for f in flags if f.startswith('-I')]
|
||||
link = [f for f in flags if f.startswith('-L') or f.startswith('-l')]
|
||||
libdirs = [f[2:] for f in link if f.startswith('-L')]
|
||||
link += ['-Wl,-rpath,' + d for d in libdirs]
|
||||
return Library(name, include, [], link, library_version(name, include))
|
||||
prefix = output(['brew', '--prefix', name]) if shutil.which('brew') else ''
|
||||
if prefix and os.path.isdir(os.path.join(prefix, 'include')):
|
||||
include = [os.path.join(prefix, 'include')]
|
||||
link = []
|
||||
if name != 'boost':
|
||||
lib = os.path.join(prefix, 'lib')
|
||||
link = ['-L' + lib, '-l' + name, '-Wl,-rpath,' + lib]
|
||||
return Library(name, include, [], link, library_version(name, include))
|
||||
if name == 'boost':
|
||||
for d in ['/usr/include', '/usr/local/include']:
|
||||
if os.path.isfile(os.path.join(d, 'boost', 'json.hpp')):
|
||||
return Library(name, [d], [], [], library_version(name, [d]))
|
||||
return None
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
"""SHA-256 of a file, read in chunks (the Boost archive is more than 100 MB)"""
|
||||
h = hashlib.sha256()
|
||||
with open(path, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def download_library(name, work):
|
||||
"""a pinned release, downloaded and checked, or an error"""
|
||||
pin = PINNED[name]
|
||||
archive = os.path.join(work, 'download', os.path.basename(pin['url']))
|
||||
os.makedirs(os.path.dirname(archive), exist_ok=True)
|
||||
if not os.path.isfile(archive):
|
||||
print(f'downloading {pin["url"]}', flush=True)
|
||||
# the URLs are the https constants in PINNED, and the SHA-256 is checked below
|
||||
# (into a .part file first, so that an interrupted download is not kept)
|
||||
with urllib.request.urlopen(pin['url'], timeout=DOWNLOAD_TIMEOUT) as response: # nosec B310
|
||||
with open(archive + '.part', 'wb') as f:
|
||||
shutil.copyfileobj(response, f)
|
||||
os.replace(archive + '.part', archive)
|
||||
digest = sha256_file(archive)
|
||||
if digest != pin['sha256']:
|
||||
os.remove(archive) # downloaded again by the next run
|
||||
sys.exit(f'error: SHA-256 of {archive} is {digest}, expected {pin["sha256"]} (removed)')
|
||||
src = os.path.join(work, 'download', pin['dir'])
|
||||
if not os.path.isdir(src):
|
||||
# extracted into a temporary directory first, so that an interrupted extraction
|
||||
# is not mistaken for a complete one
|
||||
tmp = tempfile.mkdtemp(prefix=pin['dir'] + '.part-', dir=os.path.join(work, 'download'))
|
||||
try:
|
||||
with tarfile.open(archive) as t:
|
||||
# (the 'data' filter rejects links and paths outside the target where Python has it)
|
||||
kwargs = {'filter': 'data'} if hasattr(tarfile, 'data_filter') else {}
|
||||
t.extractall(tmp, **kwargs) # noqa: S202 (checked archive) # nosec B202
|
||||
os.rename(os.path.join(tmp, pin['dir']), src)
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
if name == 'yyjson':
|
||||
return Library(name, [os.path.join(src, 'src')], [os.path.join(src, 'src', 'yyjson.c')], [], pin['version'])
|
||||
if name == 'simdjson':
|
||||
single = os.path.join(src, 'singleheader')
|
||||
return Library(name, [single], [os.path.join(single, 'simdjson.cpp')], [], pin['version'])
|
||||
return Library(name, [src], [], [], pin['version'])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# machine description
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cpu_model():
|
||||
if sys.platform == 'darwin':
|
||||
return output(['sysctl', '-n', 'machdep.cpu.brand_string'])
|
||||
try:
|
||||
with open('/proc/cpuinfo', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
if line.startswith('model name') or line.startswith('Model'):
|
||||
return line.split(':', 1)[1].strip()
|
||||
except OSError:
|
||||
pass
|
||||
# (AArch64 Linux: /proc/cpuinfo has no model name, lscpu knows it)
|
||||
for line in output(['lscpu']).splitlines():
|
||||
if line.startswith('Model name:'):
|
||||
return line.split(':', 1)[1].strip()
|
||||
return platform.processor() or platform.machine()
|
||||
|
||||
|
||||
def git_commit():
|
||||
commit = output(['git', '-C', REPO, 'rev-parse', '--short=12', 'HEAD'])
|
||||
dirty = output(['git', '-C', REPO, 'status', '--porcelain', '--untracked-files=no'])
|
||||
return commit + (' (with local changes)' if dirty else '')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('--data', required=True, help='json_test_data directory (with nativejson-benchmark/ and jeopardy/)')
|
||||
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
|
||||
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
|
||||
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
|
||||
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
|
||||
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
|
||||
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
|
||||
args = ap.parse_args()
|
||||
# the benchmarks run in the build directory: make the paths absolute
|
||||
args.data = os.path.abspath(args.data)
|
||||
args.corpus = [os.path.abspath(f) for f in args.corpus]
|
||||
args.build_dir = os.path.abspath(args.build_dir)
|
||||
|
||||
# (CXX and CC may contain arguments, e.g. 'ccache g++')
|
||||
cxx = shlex.split(os.environ.get('CXX', 'c++'))
|
||||
cc = shlex.split(os.environ.get('CC', 'cc'))
|
||||
os.makedirs(args.build_dir, exist_ok=True)
|
||||
|
||||
libs = {}
|
||||
for name in ['yyjson', 'simdjson', 'boost']:
|
||||
if name == 'boost' and args.no_boost:
|
||||
continue
|
||||
lib = download_library(name, args.build_dir) if args.download else system_library(name)
|
||||
if lib is None and name != 'boost':
|
||||
sys.exit(f'error: {name} not found; install it, or use --download')
|
||||
if lib is not None:
|
||||
libs[name] = lib
|
||||
with_boost = 'boost' in libs
|
||||
if not with_boost:
|
||||
print('Boost.JSON not found: its columns are skipped', flush=True)
|
||||
|
||||
flags = ['-std=c++17', '-O3', '-DNDEBUG', f'-DJSON_VIEW_BENCH_BOOST={1 if with_boost else 0}']
|
||||
if args.native:
|
||||
flags.append('-mcpu=native' if platform.machine().lower() in ('arm64', 'aarch64') else '-march=native')
|
||||
include = ['-I' + os.path.join(REPO, 'include')] + ['-I' + d for lib in libs.values() for d in lib.include]
|
||||
link = [f for lib in libs.values() for f in lib.link]
|
||||
|
||||
# C sources of downloaded libraries are compiled once
|
||||
objects = []
|
||||
for lib in libs.values():
|
||||
for src in lib.sources:
|
||||
obj = os.path.join(args.build_dir, os.path.basename(src) + '.o')
|
||||
is_c = src.endswith('.c')
|
||||
run((cc if is_c else cxx) + ([] if is_c else ['-std=c++17']) + ['-O3', '-DNDEBUG', '-c', src, '-o', obj]
|
||||
+ ['-I' + d for d in lib.include])
|
||||
objects.append(obj)
|
||||
|
||||
binaries = {}
|
||||
for bench in ['bench_view', 'bench_corpus', 'bench_edit']:
|
||||
exe = os.path.join(args.build_dir, bench)
|
||||
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
|
||||
binaries[bench] = exe
|
||||
|
||||
# run: bench_view on its documents, bench_corpus on those and the given files
|
||||
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
|
||||
outputs = {}
|
||||
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
|
||||
capture_output=True, text=True).stdout
|
||||
for name, text in outputs.items():
|
||||
print(text)
|
||||
|
||||
# results with their metadata
|
||||
now = datetime.datetime.now()
|
||||
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
|
||||
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
|
||||
os.makedirs(os.path.dirname(stem), exist_ok=True)
|
||||
cxx_version = output(cxx + ['--version'])
|
||||
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
|
||||
meta = [
|
||||
('date', f'{now:%Y-%m-%d %H:%M}'),
|
||||
('commit', git_commit()),
|
||||
('CPU', cpu_model()),
|
||||
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
|
||||
('compiler', compiler_version),
|
||||
('flags', ' '.join(flags)),
|
||||
('yyjson', libs['yyjson'].version),
|
||||
('simdjson', libs['simdjson'].version),
|
||||
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
|
||||
('libraries from', 'pinned downloads' if args.download else 'the system'),
|
||||
('rounds', str(args.rounds)),
|
||||
]
|
||||
with open(stem + '.md', 'w', encoding='utf-8') as f:
|
||||
f.write(f'# json_view comparison, {now:%Y-%m-%d}\n\n')
|
||||
f.write('Generated by `tests/benchmarks/json_view/compare.py`; best of the interleaved rounds.\n\n')
|
||||
f.write('| | |\n|---|---|\n')
|
||||
for key, value in meta:
|
||||
f.write(f'| {key} | {value} |\n')
|
||||
for name, text in outputs.items():
|
||||
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
|
||||
with open(stem + '.csv', 'w', encoding='utf-8') as out:
|
||||
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
|
||||
for name in ['bench_view', 'bench_corpus', 'bench_edit']:
|
||||
path = os.path.join(args.build_dir, name + '.csv')
|
||||
if os.path.isfile(path):
|
||||
with open(path, encoding='utf-8') as f:
|
||||
out.write(f'# {name}\n' + f.read())
|
||||
print(f'results: {stem}.md, {stem}.csv')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -9,12 +9,15 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <test_data.hpp>
|
||||
|
||||
@@ -710,6 +713,584 @@ static void FromCborChunkedString(benchmark::State& state, const std::size_t chu
|
||||
|
||||
BENCHMARK_CAPTURE(FromCborChunkedString, 10000 chunks, 10000);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// SAX-parse JSON from string
|
||||
//
|
||||
// sax_parse() with a handler that does next to nothing runs the lexer and
|
||||
// parser without building values, so the gap to ParseString above is the cost
|
||||
// of building the DOM.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::string read_file(const char* filename)
|
||||
{
|
||||
std::ifstream f(filename);
|
||||
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static json load_json(const char* filename)
|
||||
{
|
||||
return json::parse(read_file(filename));
|
||||
}
|
||||
|
||||
/// SAX handler that only counts the events, so that the handler costs next to nothing
|
||||
class counting_sax : public nlohmann::json_sax<json>
|
||||
{
|
||||
public:
|
||||
std::size_t events = 0;
|
||||
|
||||
bool null() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool boolean(bool /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_integer(number_integer_t /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_unsigned(number_unsigned_t /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool number_float(number_float_t /*val*/, const string_t& /*s*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool string(string_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool binary(binary_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool start_object(std::size_t /*elements*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool key(string_t& /*val*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool end_object() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool start_array(std::size_t /*elements*/) override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool end_array() override
|
||||
{
|
||||
return count();
|
||||
}
|
||||
bool parse_error(std::size_t /*position*/, const std::string& /*last_token*/, const json::exception& /*ex*/) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool count()
|
||||
{
|
||||
++events;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static void SaxParse(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::string str = read_file(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
counting_sax sax;
|
||||
bool result = json::sax_parse(str, &sax);
|
||||
benchmark::DoNotOptimize(result);
|
||||
benchmark::DoNotOptimize(sax.events);
|
||||
}
|
||||
|
||||
state.SetBytesProcessed(state.iterations() * str.size());
|
||||
}
|
||||
BENCHMARK_CAPTURE(SaxParse, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(SaxParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(SaxParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// work with values: copy, destroy, compare, iterate
|
||||
//
|
||||
// The parsed test files stand in for a document an application keeps around.
|
||||
// Each benchmark times one whole-document operation; setup and cleanup run
|
||||
// outside the timed section.
|
||||
//
|
||||
// A copy is always destroyed again, and how a document is freed shapes the
|
||||
// heap the next copy allocates from: a destructor that frees quickly can leave
|
||||
// work to the allocator's next calls. Copy, CopyAssign and CopyRing therefore
|
||||
// time the copy together with the destruction of a copy; CopyOnly and
|
||||
// DestroyOnly time one half each and are meant for telling the halves apart,
|
||||
// not for comparing versions.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// copy a document and destroy the copy again, as a temporary copy is used
|
||||
static void Copy(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json copy = j;
|
||||
benchmark::DoNotOptimize(copy);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Copy, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Copy, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Copy, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// copy-assign a document over an earlier copy of it, which is freed in the process
|
||||
static void CopyAssign(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
json target = j;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
target = j;
|
||||
benchmark::DoNotOptimize(target);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(CopyAssign, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(CopyAssign, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(CopyAssign, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// keep eight copies alive and replace the oldest one, as a cache or an undo
|
||||
/// history does, so that the copy is not made from memory just freed by an
|
||||
/// identical document
|
||||
static void CopyRing(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
std::vector<json> ring(8, j);
|
||||
std::size_t oldest = 0;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
ring[oldest] = j;
|
||||
benchmark::DoNotOptimize(ring[oldest]);
|
||||
oldest = (oldest + 1) % ring.size();
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(CopyRing, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(CopyRing, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(CopyRing, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// only the copy; the copy is destroyed with the timer paused (diagnostic)
|
||||
static void CopyOnly(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto* copy = new json(j);
|
||||
|
||||
state.PauseTiming();
|
||||
delete copy;
|
||||
state.ResumeTiming();
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(CopyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(CopyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(CopyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// only the destruction; the copy is made with the timer paused (diagnostic)
|
||||
static void DestroyOnly(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
state.PauseTiming();
|
||||
auto* copy = new json(j);
|
||||
state.ResumeTiming();
|
||||
|
||||
delete copy;
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(DestroyOnly, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(DestroyOnly, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(DestroyOnly, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void Equal(benchmark::State& state, const char* filename)
|
||||
{
|
||||
// two separate values, so that the comparison cannot stop at identical addresses
|
||||
const json j = load_json(filename);
|
||||
const json copy = j;
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
bool result = (j == copy);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Equal, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Equal, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Equal, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
/// visit every value with range-based for loops, as client code walks a document
|
||||
static std::size_t count_values(const json& j)
|
||||
{
|
||||
std::size_t result = 1;
|
||||
if (j.is_structured())
|
||||
{
|
||||
for (const auto& element : j)
|
||||
{
|
||||
result += count_values(element);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void Iterate(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
std::size_t result = count_values(j);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(count_values(j)));
|
||||
}
|
||||
BENCHMARK_CAPTURE(Iterate, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Iterate, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Iterate, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// flatten and unflatten
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void Flatten(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json flat = j.flatten();
|
||||
benchmark::DoNotOptimize(flat);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Flatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Flatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Flatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
// ordered_json objects look keys up linearly, which flatten() used to do for
|
||||
// every value; canada (330,000 values in one flat object) is left out, as it
|
||||
// would take minutes with such versions
|
||||
static void FlattenOrdered(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const nlohmann::ordered_json j = nlohmann::ordered_json::parse(read_file(filename));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
nlohmann::ordered_json flat = j.flatten();
|
||||
benchmark::DoNotOptimize(flat);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(FlattenOrdered, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(FlattenOrdered, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void Unflatten(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json flat = load_json(filename).flatten();
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json j = flat.unflatten();
|
||||
benchmark::DoNotOptimize(j);
|
||||
}
|
||||
}
|
||||
BENCHMARK_CAPTURE(Unflatten, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Unflatten, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Unflatten, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// diff and patch
|
||||
//
|
||||
// The target is the document with every eighth scalar changed, so the patch
|
||||
// has one "replace" operation per changed value and the structure stays the
|
||||
// same.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void change_every_eighth_scalar(json& j, std::size_t& counter)
|
||||
{
|
||||
if (j.is_structured())
|
||||
{
|
||||
for (auto& element : j)
|
||||
{
|
||||
change_every_eighth_scalar(element, counter);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (++counter % 8 != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (j.type())
|
||||
{
|
||||
case json::value_t::number_integer:
|
||||
j = j.get<json::number_integer_t>() + 1;
|
||||
break;
|
||||
case json::value_t::number_unsigned:
|
||||
j = j.get<json::number_unsigned_t>() + 1;
|
||||
break;
|
||||
case json::value_t::number_float:
|
||||
j = j.get<json::number_float_t>() + 0.5;
|
||||
break;
|
||||
case json::value_t::string:
|
||||
j = j.get<std::string>() + "!";
|
||||
break;
|
||||
case json::value_t::boolean:
|
||||
j = !j.get<bool>();
|
||||
break;
|
||||
case json::value_t::null:
|
||||
case json::value_t::object:
|
||||
case json::value_t::array:
|
||||
case json::value_t::binary:
|
||||
case json::value_t::discarded:
|
||||
default:
|
||||
j = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static json changed_copy(const json& j)
|
||||
{
|
||||
json result = j;
|
||||
std::size_t counter = 0;
|
||||
change_every_eighth_scalar(result, counter);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void Diff(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json source = load_json(filename);
|
||||
const json target = changed_copy(source);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json patch = json::diff(source, target);
|
||||
benchmark::DoNotOptimize(patch);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(json::diff(source, target).size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(Diff, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Diff, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Diff, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void Patch(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json source = load_json(filename);
|
||||
const json patch = json::diff(source, changed_copy(source));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
json result = source.patch(patch);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(patch.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(Patch, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
|
||||
BENCHMARK_CAPTURE(Patch, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(Patch, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// JSON Pointer
|
||||
//
|
||||
// The pointers are those of every scalar in the document, i.e., the keys of
|
||||
// its flattened form.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static std::vector<std::string> pointer_strings(const json& j)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
const json flat = j.flatten();
|
||||
result.reserve(flat.size());
|
||||
for (auto it = flat.begin(); it != flat.end(); ++it)
|
||||
{
|
||||
result.push_back(it.key());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<json::json_pointer> pointers(const json& j)
|
||||
{
|
||||
const std::vector<std::string> strings = pointer_strings(j);
|
||||
std::vector<json::json_pointer> result;
|
||||
result.reserve(strings.size());
|
||||
for (const auto& s : strings)
|
||||
{
|
||||
result.emplace_back(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void JsonPointerParse(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::vector<std::string> strings = pointer_strings(load_json(filename));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& s : strings)
|
||||
{
|
||||
json::json_pointer ptr(s);
|
||||
benchmark::DoNotOptimize(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(strings.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerParse, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerParse, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void JsonPointerToString(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const std::vector<json::json_pointer> ptrs = pointers(load_json(filename));
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& ptr : ptrs)
|
||||
{
|
||||
std::string s = ptr.to_string();
|
||||
benchmark::DoNotOptimize(s);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerToString, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerToString, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void JsonPointerAt(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
const std::vector<json::json_pointer> ptrs = pointers(j);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& ptr : ptrs)
|
||||
{
|
||||
const json& value = j.at(ptr);
|
||||
benchmark::DoNotOptimize(&value);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerAt, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerAt, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
static void JsonPointerContains(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
const std::vector<json::json_pointer> ptrs = pointers(j);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
for (const auto& ptr : ptrs)
|
||||
{
|
||||
bool result = j.contains(ptr);
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(ptrs.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(JsonPointerContains, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
BENCHMARK_CAPTURE(JsonPointerContains, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// conversions from and to C++ containers
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void GetVectorInt(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto v = j.get<std::vector<std::int64_t>>();
|
||||
benchmark::DoNotOptimize(v);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(GetVectorInt, signed_ints, TEST_DATA_DIRECTORY "/regression/signed_ints.json");
|
||||
|
||||
static void GetVectorDouble(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename);
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto v = j.get<std::vector<double>>();
|
||||
benchmark::DoNotOptimize(v);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(GetVectorDouble, floats, TEST_DATA_DIRECTORY "/regression/floats.json");
|
||||
|
||||
// a large flat object: the flattened document, one member per scalar
|
||||
static void GetMap(benchmark::State& state, const char* filename)
|
||||
{
|
||||
const json j = load_json(filename).flatten();
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto m = j.get<std::map<std::string, json>>();
|
||||
benchmark::DoNotOptimize(m);
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(j.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(GetMap, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
|
||||
|
||||
/// 100,000 strings, each too long for the small string optimization
|
||||
static std::vector<std::string> make_strings()
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(100000);
|
||||
for (std::size_t i = 0; i < 100000; ++i)
|
||||
{
|
||||
result.push_back("a string that does not fit in place: " + std::to_string(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void ToJsonStrings(benchmark::State& state, const bool move)
|
||||
{
|
||||
const std::vector<std::string> strings = make_strings();
|
||||
|
||||
for (auto _ : state)
|
||||
{
|
||||
state.PauseTiming();
|
||||
std::vector<std::string> input = strings;
|
||||
state.ResumeTiming();
|
||||
|
||||
// destroyed inside the timed section, like the copies above
|
||||
auto* j = move ? new json(std::move(input)) : new json(input);
|
||||
delete j;
|
||||
}
|
||||
|
||||
state.SetItemsProcessed(state.iterations() * static_cast<std::int64_t>(strings.size()));
|
||||
}
|
||||
BENCHMARK_CAPTURE(ToJsonStrings, copy, false);
|
||||
BENCHMARK_CAPTURE(ToJsonStrings, move, true);
|
||||
|
||||
// conversion of a JSON array into standard containers
|
||||
template<typename Container>
|
||||
static void GetArray(benchmark::State& state)
|
||||
|
||||
@@ -492,3 +492,278 @@ TEST_CASE("binary_reserve_hint never over-reserves")
|
||||
CHECK(nlohmann::detail::binary_reserve_hint(json({{"a", 1}, {"b", 2}})) == 5);
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// numbers around every boundary of the integer encodings, and floats of each
|
||||
// kind (exact in float, needing double, special values)
|
||||
std::vector<json> boundary_numbers()
|
||||
{
|
||||
std::vector<json> numbers;
|
||||
for (int k = 0; k < 64; ++k)
|
||||
{
|
||||
const std::uint64_t p = std::uint64_t{1} << k;
|
||||
for (const std::uint64_t d :
|
||||
{
|
||||
p - 1, p, p + 1
|
||||
})
|
||||
{
|
||||
numbers.emplace_back(d);
|
||||
if (d <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
|
||||
{
|
||||
numbers.emplace_back(static_cast<std::int64_t>(d));
|
||||
numbers.emplace_back(-static_cast<std::int64_t>(d));
|
||||
numbers.emplace_back(-static_cast<std::int64_t>(d) - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
numbers.emplace_back((std::numeric_limits<std::int64_t>::min)());
|
||||
numbers.emplace_back((std::numeric_limits<std::uint64_t>::max)());
|
||||
for (const double f :
|
||||
{
|
||||
0.0, -0.0, 1.0, -1.0, 0.5, 0.1, 3.25, 1e300, -1e-300, 3.4028234663852886e38, 1e39,
|
||||
std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::quiet_NaN()
|
||||
})
|
||||
{
|
||||
numbers.emplace_back(f);
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
// the concatenation of the encodings of the elements of @a numbers[first, first + count)
|
||||
template<typename Single>
|
||||
std::vector<std::uint8_t> concat_singles(const std::vector<json>& numbers, const std::size_t first, const std::size_t count, Single single)
|
||||
{
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (std::size_t i = first; i < first + count; ++i)
|
||||
{
|
||||
const auto one = single(numbers[i]);
|
||||
bytes.insert(bytes.end(), one.begin(), one.end());
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Runs of numbers in arrays are encoded straight into space reserved from the
|
||||
// sink, by code that is separate from the encoding of a single value. Both have
|
||||
// to agree on every number: an array is its head and the encodings of its
|
||||
// elements, one after the other.
|
||||
TEST_CASE("runs of numbers in arrays are encoded as the numbers one by one")
|
||||
{
|
||||
const std::vector<json> numbers = boundary_numbers();
|
||||
|
||||
// arrays of every length that a one-byte head covers, so that runs are as
|
||||
// long as, and longer and shorter than, what fits one reservation
|
||||
for (const std::size_t count :
|
||||
{
|
||||
1u, 2u, 3u, 5u, 14u, 15u, 23u
|
||||
})
|
||||
{
|
||||
for (std::size_t first = 0; first + count <= numbers.size(); first += 7)
|
||||
{
|
||||
const json array(std::vector<json>(numbers.begin() + static_cast<std::ptrdiff_t>(first),
|
||||
numbers.begin() + static_cast<std::ptrdiff_t>(first + count)));
|
||||
CAPTURE(array.dump(-1, ' ', false, json::error_handler_t::replace))
|
||||
|
||||
std::vector<std::uint8_t> expected{static_cast<std::uint8_t>(0x80 + count)};
|
||||
auto elements = concat_singles(numbers, first, count, [](const json & j)
|
||||
{
|
||||
return json::to_cbor(j);
|
||||
});
|
||||
expected.insert(expected.end(), elements.begin(), elements.end());
|
||||
CHECK(json::to_cbor(array) == expected);
|
||||
|
||||
expected.assign(1, static_cast<std::uint8_t>(count <= 15 ? 0x90 + count : 0xDC));
|
||||
if (count > 15)
|
||||
{
|
||||
expected.push_back(0);
|
||||
expected.push_back(static_cast<std::uint8_t>(count));
|
||||
}
|
||||
elements = concat_singles(numbers, first, count, [](const json & j)
|
||||
{
|
||||
return json::to_msgpack(j);
|
||||
});
|
||||
expected.insert(expected.end(), elements.begin(), elements.end());
|
||||
CHECK(json::to_msgpack(array) == expected);
|
||||
|
||||
for (const bool bjdata :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
expected.assign(1, static_cast<std::uint8_t>('['));
|
||||
elements = concat_singles(numbers, first, count, [bjdata](const json & j)
|
||||
{
|
||||
return bjdata ? json::to_bjdata(j) : json::to_ubjson(j);
|
||||
});
|
||||
expected.insert(expected.end(), elements.begin(), elements.end());
|
||||
expected.push_back(static_cast<std::uint8_t>(']'));
|
||||
CHECK((bjdata ? json::to_bjdata(array) : json::to_ubjson(array)) == expected);
|
||||
}
|
||||
|
||||
// BON8 has no integers above the int64 range
|
||||
bool representable = true;
|
||||
for (std::size_t i = first; i < first + count; ++i)
|
||||
{
|
||||
representable = representable && bon8_representable(numbers[i]);
|
||||
}
|
||||
if (representable)
|
||||
{
|
||||
expected.assign(1, static_cast<std::uint8_t>(count <= 4 ? 0x80 + count : 0x85));
|
||||
elements = concat_singles(numbers, first, count, [](const json & j)
|
||||
{
|
||||
return json::to_bon8(j);
|
||||
});
|
||||
expected.insert(expected.end(), elements.begin(), elements.end());
|
||||
if (count > 4)
|
||||
{
|
||||
expected.push_back(0xFE);
|
||||
}
|
||||
CHECK(json::to_bon8(array) == expected);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK_THROWS_AS(json::to_bon8(array), json::out_of_range&);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An array that is written in more than one reservation (more numbers than fit
|
||||
// one), and arrays of an index with more digits, round trip for every format.
|
||||
TEST_CASE("large arrays and many digits of indices round trip")
|
||||
{
|
||||
json array = json::array();
|
||||
for (int i = 0; i < 12000; ++i)
|
||||
{
|
||||
switch (i % 4)
|
||||
{
|
||||
case 0:
|
||||
array.push_back(i);
|
||||
break;
|
||||
case 1:
|
||||
array.push_back(-i);
|
||||
break;
|
||||
case 2:
|
||||
array.push_back(i * 0.25);
|
||||
break;
|
||||
default:
|
||||
array.push_back(i % 8 == 3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
json document = json::object();
|
||||
document["numbers"] = array;
|
||||
document["nested"] = json::array({array, "text", array});
|
||||
|
||||
CHECK(json::from_cbor(json::to_cbor(document)) == document);
|
||||
CHECK(json::from_msgpack(json::to_msgpack(document)) == document);
|
||||
CHECK(json::from_ubjson(json::to_ubjson(document)) == document);
|
||||
CHECK(json::from_ubjson(json::to_ubjson(document, true, true)) == document);
|
||||
CHECK(json::from_bjdata(json::to_bjdata(document)) == document);
|
||||
CHECK(json::from_bson(json::to_bson(document)) == document);
|
||||
CHECK(json::from_bon8(json::to_bon8(document)) == document);
|
||||
}
|
||||
|
||||
// Strings and keys are written with their head in one reservation up to a
|
||||
// length; around that length, and with every kind of head, they must read back.
|
||||
TEST_CASE("strings of every head length round trip")
|
||||
{
|
||||
json document = json::object();
|
||||
json array = json::array();
|
||||
for (const std::size_t length :
|
||||
{
|
||||
0u, 1u, 2u, 3u, 4u, 7u, 8u, 15u, 16u, 17u, 23u, 24u, 25u, 31u, 32u, 33u, 63u, 127u, 128u, 129u, 200u, 245u, 246u, 247u, 248u, 249u, 250u, 251u, 252u, 253u, 254u, 255u, 256u, 257u, 300u, 1000u, 65535u, 65536u, 70000u
|
||||
})
|
||||
{
|
||||
const std::string text(length, 'x');
|
||||
array.push_back(text);
|
||||
document[std::string(length, 'k')] = text;
|
||||
}
|
||||
document["array"] = array;
|
||||
|
||||
CHECK(json::from_cbor(json::to_cbor(document)) == document);
|
||||
CHECK(json::from_msgpack(json::to_msgpack(document)) == document);
|
||||
CHECK(json::from_ubjson(json::to_ubjson(document)) == document);
|
||||
CHECK(json::from_ubjson(json::to_ubjson(document, true, true)) == document);
|
||||
CHECK(json::from_bjdata(json::to_bjdata(document)) == document);
|
||||
CHECK(json::from_bson(json::to_bson(document)) == document);
|
||||
CHECK(json::from_bon8(json::to_bon8(document)) == document);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// What a write that throws leaves behind in an output adapter is what was written
|
||||
// before it, whatever the adapter and however the adapter sink buffers. (Without
|
||||
// exceptions, the writes in CHECK_THROWS_AS do not run.)
|
||||
TEST_CASE("a write that throws leaves the same bytes in every kind of output")
|
||||
{
|
||||
json array = json::array();
|
||||
for (int i = 0; i < 700; ++i)
|
||||
{
|
||||
array.push_back(i * 1000);
|
||||
array.push_back("text " + std::to_string(i));
|
||||
}
|
||||
array.push_back("bad \xFF byte");
|
||||
json document = json::object();
|
||||
document["first"] = array;
|
||||
document["second"] = 1;
|
||||
|
||||
const auto strict = json::error_handler_t::strict;
|
||||
|
||||
SECTION("cbor")
|
||||
{
|
||||
std::vector<std::uint8_t> as_vector;
|
||||
std::string as_string;
|
||||
std::ostringstream as_stream;
|
||||
CHECK_THROWS_AS(json::to_cbor(document, as_vector, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_cbor(document, as_string, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_cbor(document, as_stream, strict), json::type_error&);
|
||||
CHECK(!as_vector.empty());
|
||||
CHECK(std::string(reinterpret_cast<const char*>(as_vector.data()), as_vector.size()) == as_string); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
CHECK(as_stream.str() == as_string);
|
||||
}
|
||||
|
||||
SECTION("msgpack")
|
||||
{
|
||||
std::vector<std::uint8_t> as_vector;
|
||||
std::string as_string;
|
||||
std::ostringstream as_stream;
|
||||
CHECK_THROWS_AS(json::to_msgpack(document, as_vector, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_msgpack(document, as_string, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_msgpack(document, as_stream, strict), json::type_error&);
|
||||
CHECK(!as_vector.empty());
|
||||
CHECK(std::string(reinterpret_cast<const char*>(as_vector.data()), as_vector.size()) == as_string); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
CHECK(as_stream.str() == as_string);
|
||||
}
|
||||
|
||||
SECTION("ubjson")
|
||||
{
|
||||
std::vector<std::uint8_t> as_vector;
|
||||
std::string as_string;
|
||||
std::ostringstream as_stream;
|
||||
CHECK_THROWS_AS(json::to_ubjson(document, as_vector, false, false, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_ubjson(document, as_string, false, false, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_ubjson(document, as_stream, false, false, strict), json::type_error&);
|
||||
CHECK(!as_vector.empty());
|
||||
CHECK(std::string(reinterpret_cast<const char*>(as_vector.data()), as_vector.size()) == as_string); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
CHECK(as_stream.str() == as_string);
|
||||
}
|
||||
|
||||
SECTION("bson")
|
||||
{
|
||||
// the size pass finds the string before anything is written
|
||||
std::vector<std::uint8_t> as_vector;
|
||||
std::string as_string;
|
||||
std::ostringstream as_stream;
|
||||
CHECK_THROWS_AS(json::to_bson(document, as_vector, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_bson(document, as_string, strict), json::type_error&);
|
||||
CHECK_THROWS_AS(json::to_bson(document, as_stream, strict), json::type_error&);
|
||||
CHECK(as_vector.empty());
|
||||
CHECK(as_string.empty());
|
||||
CHECK(as_stream.str().empty());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -13,11 +13,15 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array> // array
|
||||
#include <cctype> // toupper
|
||||
#include <cstdint> // uint32_t, uint64_t
|
||||
#include <cstdio> // snprintf
|
||||
#include <cmath> // isfinite
|
||||
#include <cstdlib> // strtod
|
||||
#include <cstring> // memcpy
|
||||
#include <limits> // numeric_limits
|
||||
#include <initializer_list> // initializer_list
|
||||
#include <list> // list
|
||||
#include <map> // map
|
||||
#include <random> // mt19937
|
||||
#include <sstream> // stringstream
|
||||
@@ -332,6 +336,112 @@ TEST_CASE("lexer number fast path")
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// this section reports parse errors, which abort when exceptions are off
|
||||
SECTION("digits accumulated while scanning")
|
||||
{
|
||||
// The fast path accumulates the digits while it validates them, eight
|
||||
// at a time where possible, and converts tokens with at most 19
|
||||
// digits (floats) or 18 digits (integers) from that value. Every digit
|
||||
// count from 1 to 25, in the integer and in the fraction part, with
|
||||
// and without sign and exponent, must give the byte path's result. A
|
||||
// std::list has no contiguous storage, so parsing it takes the byte
|
||||
// path.
|
||||
// type and value, or the error (values beyond the range of double
|
||||
// are rejected with out_of_range.406 on both paths)
|
||||
const auto outcome = [](const std::string & doc, bool byte_path) -> std::string
|
||||
{
|
||||
JSON_TRY
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
const json j = byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc);
|
||||
return std::string(j[0].type_name()) + '|' + j.dump();
|
||||
}
|
||||
JSON_CATCH (const json::exception & e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
};
|
||||
|
||||
// the concatenation of @a parts
|
||||
const auto join = [](std::initializer_list<std::string> parts)
|
||||
{
|
||||
std::string result;
|
||||
for (const auto& part : parts)
|
||||
{
|
||||
result += part;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
std::vector<std::string> numbers;
|
||||
for (std::size_t digits = 1; digits <= 25; ++digits)
|
||||
{
|
||||
std::string run;
|
||||
for (std::size_t i = 0; i < digits; ++i)
|
||||
{
|
||||
run += static_cast<char>('1' + static_cast<char>((i * 7) % 9));
|
||||
}
|
||||
const std::string nines(digits, '9');
|
||||
for (const char* sign_text :
|
||||
{"", "-"
|
||||
})
|
||||
{
|
||||
const std::string sign = sign_text;
|
||||
numbers.push_back(join({sign, run}));
|
||||
numbers.push_back(join({sign, nines}));
|
||||
numbers.push_back(join({sign, "1", std::string(digits - 1, '0')}));
|
||||
numbers.push_back(join({sign, "0.", run}));
|
||||
numbers.push_back(join({sign, "0.", std::string(digits, '0'), "1"}));
|
||||
numbers.push_back(join({sign, run, ".", run}));
|
||||
numbers.push_back(join({sign, run, "e-", std::to_string(digits)}));
|
||||
numbers.push_back(join({sign, "1.", run, "E+", std::to_string(digits * 13)}));
|
||||
}
|
||||
}
|
||||
// exponents beyond every range, and zeros
|
||||
numbers.insert(numbers.end(), {"1e400", "1e-400", "-1e99999999999999999999", "1e-99999999999999999999",
|
||||
"0e99999999999999999999", "0.0e-5", "-0.000", "123456789e-0000000000000000000000330"
|
||||
});
|
||||
|
||||
for (const auto& n : numbers)
|
||||
{
|
||||
CAPTURE(n)
|
||||
const std::string doc = "[" + n + "]";
|
||||
const std::string a = outcome(doc, false);
|
||||
CHECK(a == outcome(doc, true));
|
||||
const double expected = std::strtod(n.c_str(), nullptr);
|
||||
if (a.compare(0, 6, "number") == 0 && std::isfinite(expected) && n.find_first_of(".eE") != std::string::npos)
|
||||
{
|
||||
CHECK(json::parse(doc)[0].get<double>() == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("integers that do not fit a narrower type become floats")
|
||||
{
|
||||
// up to 18 digits, the fast path stores the accumulated value without
|
||||
// an overflow check; a narrower number_integer_t/number_unsigned_t
|
||||
// must still see the value rejected and converted as a float
|
||||
using json32 = nlohmann::basic_json<std::map, std::vector, std::string, bool, std::int32_t, std::uint32_t, float>;
|
||||
for (const char* n :
|
||||
{"4294967295", "4294967296", "2147483647", "-2147483648", "-2147483649", "99999999999", "-99999999999"
|
||||
})
|
||||
{
|
||||
CAPTURE(n)
|
||||
const std::string doc = std::string("[") + n + "]";
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
const json32 a = json32::parse(doc);
|
||||
const json32 b = json32::parse(chars.begin(), chars.end());
|
||||
CHECK(a[0].type() == b[0].type());
|
||||
CHECK(a == b);
|
||||
}
|
||||
CHECK(json32::parse("[4294967295]")[0].is_number_unsigned());
|
||||
CHECK(json32::parse("[4294967296]")[0].is_number_float());
|
||||
CHECK(json32::parse("[-2147483648]")[0].is_number_integer());
|
||||
CHECK(json32::parse("[-2147483649]")[0].is_number_float());
|
||||
}
|
||||
|
||||
SECTION("token type classification")
|
||||
{
|
||||
CHECK((scan_string("0") == json::lexer::token_type::value_unsigned));
|
||||
@@ -490,6 +600,67 @@ TEST_CASE("lexer number fast path")
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// this test case compares the errors of both lexer paths, which abort when
|
||||
// exceptions are off
|
||||
TEST_CASE("lexer literal fast path")
|
||||
{
|
||||
// For contiguous input, the rest of a literal is compared at once, and a
|
||||
// mismatch is left to the byte path, which reports the error at the
|
||||
// character that differs. Every truncation and every wrong character of
|
||||
// each literal, at the end of the input or followed by more text, must
|
||||
// give the byte path's result (a std::list is read one character at a
|
||||
// time).
|
||||
const auto outcome = [](const std::string & doc, bool byte_path) -> std::string
|
||||
{
|
||||
JSON_TRY
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
return (byte_path ? json::parse(chars.begin(), chars.end()) : json::parse(doc)).dump();
|
||||
}
|
||||
JSON_CATCH (const json::exception & e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::string> mismatches;
|
||||
for (const std::string literal :
|
||||
{"true", "false", "null"
|
||||
})
|
||||
{
|
||||
std::vector<std::string> variants;
|
||||
for (std::size_t length = 1; length <= literal.size(); ++length)
|
||||
{
|
||||
variants.push_back(literal.substr(0, length));
|
||||
}
|
||||
for (std::size_t i = 1; i < literal.size(); ++i)
|
||||
{
|
||||
std::string changed = literal;
|
||||
changed[i] = 'X';
|
||||
variants.push_back(changed);
|
||||
changed[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(literal[i])));
|
||||
variants.push_back(changed);
|
||||
}
|
||||
for (const auto& variant : variants)
|
||||
{
|
||||
for (const std::string& doc :
|
||||
{
|
||||
variant, "[" + variant + "]", "[" + variant, variant + " ", variant + "1", "{\"k\":" + variant + "}"
|
||||
})
|
||||
{
|
||||
if (outcome(doc, false) != outcome(doc, true))
|
||||
{
|
||||
mismatches.push_back(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CAPTURE(mismatches)
|
||||
CHECK(mismatches.empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("lexer string fast path")
|
||||
{
|
||||
// Build a byte string from explicit values: a hex escape in a string
|
||||
@@ -610,6 +781,73 @@ TEST_CASE("lexer string fast path")
|
||||
CAPTURE(mismatches)
|
||||
CHECK(mismatches.empty());
|
||||
}
|
||||
|
||||
SECTION("special bytes at every offset of the vector blocks")
|
||||
{
|
||||
// With SSE2 or NEON, runs are scanned 16 bytes at a time (after one
|
||||
// SWAR word, or four with NEON), and runs of non-ASCII text are
|
||||
// checked 16 bytes at a time as well. Place every kind of byte that
|
||||
// ends a run at each offset across several blocks, after ASCII and
|
||||
// after CJK text, so sequences also straddle the block boundaries.
|
||||
// The reference is a std::list, which has no contiguous storage and
|
||||
// is therefore read one character at a time.
|
||||
const auto byte_path = [](const std::string & doc) -> std::string
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
JSON_TRY
|
||||
{
|
||||
return json::parse(chars.begin(), chars.end()).dump();
|
||||
}
|
||||
JSON_CATCH (const json::exception & e)
|
||||
{
|
||||
return e.what();
|
||||
}
|
||||
};
|
||||
const std::vector<std::string> specials =
|
||||
{
|
||||
"\"", "\\", "\\u00e9", bytes({0x01}), bytes({0x1F}), bytes({0x7F}),
|
||||
bytes({0xC3, 0xA9}), bytes({0xE4, 0xB8, 0xAD}), bytes({0xF0, 0x9F, 0x98, 0x80}),
|
||||
bytes({0xFF}), bytes({0xC3}), bytes({0xE4, 0xB8}), bytes({0xED, 0xA0, 0x80}), bytes({0xC0, 0x80})
|
||||
};
|
||||
const std::string cjk = bytes({0xE4, 0xB8, 0xAD}); // U+4E2D
|
||||
|
||||
std::vector<std::string> mismatches;
|
||||
for (std::size_t offset = 0; offset <= 80; ++offset)
|
||||
{
|
||||
std::string text;
|
||||
while (text.size() < offset)
|
||||
{
|
||||
text += cjk;
|
||||
}
|
||||
for (const auto& prefix :
|
||||
{
|
||||
std::string(offset, 'a'), text
|
||||
})
|
||||
{
|
||||
for (const auto& special : specials)
|
||||
{
|
||||
// the closing quote right behind, or in a later block
|
||||
for (const int tail :
|
||||
{
|
||||
0, 1, 3, 20
|
||||
})
|
||||
{
|
||||
std::string doc = "[\"";
|
||||
doc += prefix;
|
||||
doc += special;
|
||||
doc.append(static_cast<std::size_t>(tail), 'b');
|
||||
doc += "\"]";
|
||||
if (outcome(doc, false) != byte_path(doc))
|
||||
{
|
||||
mismatches.push_back(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CAPTURE(mismatches)
|
||||
CHECK(mismatches.empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
// json::accept() never throws, so the ranges stay covered without exceptions
|
||||
|
||||
@@ -30,7 +30,9 @@ using nlohmann::json;
|
||||
#include <valarray>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <list>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -2450,6 +2452,109 @@ std::string parse_error_message_range(IteratorType first, IteratorType last)
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("object keys are moved into the value")
|
||||
{
|
||||
// json_sax_dom_parser::key() moves the key out of the lexer's buffer into
|
||||
// the object, and the lexer starts the next token with an empty buffer.
|
||||
// Keys of every length (inline and allocated strings), keys with escapes,
|
||||
// and duplicate keys must give the same objects as reading the input one
|
||||
// character at a time, and as ordered_json, whose map copies the key.
|
||||
const std::string long_key(100, 'k');
|
||||
const std::string doc = R"({"a": 1, ")" + long_key + R"(": {"b": [2, 3], "sixteen_chars_xx": 4}, )"
|
||||
R"("esc\"apedé": 5, "a": 6, ")" + long_key + R"(x": 7, "": 8, "dup": 9, "dup": 10})";
|
||||
|
||||
const json j = json::parse(doc);
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
CHECK(j == json::parse(chars.begin(), chars.end()));
|
||||
|
||||
CHECK(j.size() == 6);
|
||||
CHECK(j["a"] == 6);
|
||||
CHECK(j[long_key]["b"] == json({2, 3}));
|
||||
CHECK(j[long_key]["sixteen_chars_xx"] == 4);
|
||||
CHECK(j["esc\"aped\xc3\xa9"] == 5);
|
||||
CHECK(j[long_key + "x"] == 7);
|
||||
CHECK(j[""] == 8);
|
||||
CHECK(j["dup"] == 10);
|
||||
|
||||
const auto o = nlohmann::ordered_json::parse(doc);
|
||||
CHECK(o.size() == 6);
|
||||
CHECK(o[long_key + "x"] == 7);
|
||||
CHECK(json(o) == j);
|
||||
}
|
||||
|
||||
TEST_CASE("text of float tokens")
|
||||
{
|
||||
// For contiguous input, the lexer copies the text of a float token into
|
||||
// its buffer only when a SAX handler asks for it (the library's own
|
||||
// handlers do not). A user handler must still see the exact text, for
|
||||
// every input adapter and in every position of a document.
|
||||
const std::string doc = R"([1.5, -0.0, 0.1, 1e5, -2.5E-4, 12345678901234567890.5, 0.12345678901234567890123,
|
||||
{"a": 3.25, "b": [6.02e23, 1]}, "x", true, 7e-1])";
|
||||
const std::vector<std::string> expected_floats =
|
||||
{
|
||||
"number_float(1.5)", "number_float(-0.0)", "number_float(0.1)", "number_float(1e5)", "number_float(-2.5E-4)",
|
||||
"number_float(12345678901234567890.5)", "number_float(0.12345678901234567890123)", "number_float(3.25)",
|
||||
"number_float(6.02e23)", "number_float(7e-1)"
|
||||
};
|
||||
|
||||
const auto floats = [](const std::vector<std::string>& events)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
std::copy_if(events.begin(), events.end(), std::back_inserter(result), [](const std::string & e)
|
||||
{
|
||||
return e.compare(0, 13, "number_float(") == 0;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
SECTION("contiguous input")
|
||||
{
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(doc, &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("byte input")
|
||||
{
|
||||
const std::vector<std::uint8_t> bytes(doc.begin(), doc.end());
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(bytes, &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("stream input")
|
||||
{
|
||||
std::istringstream is(doc);
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(is, &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("non-contiguous iterators")
|
||||
{
|
||||
const std::list<char> chars(doc.begin(), doc.end());
|
||||
SaxEventLogger l;
|
||||
CHECK(json::sax_parse(chars.begin(), chars.end(), &l));
|
||||
CHECK(floats(l.events) == expected_floats);
|
||||
}
|
||||
|
||||
SECTION("the library's own handlers do not need the text")
|
||||
{
|
||||
using lexer_adapter = nlohmann::detail::iterator_input_adapter<const char*>;
|
||||
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_dom_parser<json, lexer_adapter>>::value);
|
||||
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_dom_callback_parser<json, lexer_adapter>>::value);
|
||||
CHECK(nlohmann::detail::sax_ignores_number_text<nlohmann::detail::json_sax_acceptor<json>>::value);
|
||||
CHECK_FALSE(nlohmann::detail::sax_ignores_number_text<SaxEventLogger>::value);
|
||||
|
||||
// and the values are the same as when the text is read
|
||||
const json j = json::parse(doc);
|
||||
CHECK(j[0] == 1.5);
|
||||
CHECK(j[5].get<double>() == 12345678901234567890.5);
|
||||
CHECK(j[7]["b"][0] == 6.02e23);
|
||||
CHECK(json::accept(doc));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("last-read diagnostics are identical across input adapters")
|
||||
{
|
||||
// The lexer reconstructs the "last read" token lazily for seekable adapters
|
||||
|
||||
@@ -285,6 +285,58 @@ TEST_CASE("custom number types: integers beyond 64 bits")
|
||||
CHECK(wide_json::to_bjdata(wide_json::array({uint_big, uint_big}), true, true) == expected_bjdata_array);
|
||||
}
|
||||
|
||||
SECTION("integers in arrays and objects")
|
||||
{
|
||||
// runs of numbers and scalar object members are encoded by loops of
|
||||
// their own for the number types up to 64 bits; a wider number type
|
||||
// takes the generic path, which checks the range of every integer
|
||||
const wide_json run = wide_json::array({make_int(1), make_int(-3), make_uint(7), wide_json(1.5), int_big, make_int(5)});
|
||||
const wide_json run_negative = wide_json::array({make_int(1), make_int(-3), int_big_negative});
|
||||
const wide_json run_unsigned = wide_json::array({make_uint(1), make_uint(7), uint_big});
|
||||
const wide_json members = wide_json::object({{"a", make_int(1)}, {"b", wide_json(1.5)}, {"c", int_big}});
|
||||
|
||||
const std::vector<wide_json> failing = {run, run_negative, run_unsigned, members, wrap(run), wrap(members)};
|
||||
for (const auto& value : failing)
|
||||
{
|
||||
CHECK_THROWS_AS(_ = wide_json::to_cbor(value), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_msgpack(value), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_bson(wrap(value)), out_of_range&);
|
||||
CHECK_THROWS_AS(_ = wide_json::to_bon8(value), out_of_range&);
|
||||
}
|
||||
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
// a failing write leaves no more than the elements before the failing one
|
||||
// (without exceptions, the writes in CHECK_THROWS_AS do not run)
|
||||
std::vector<std::uint8_t> out;
|
||||
CHECK_THROWS_AS(wide_json::to_cbor(run, out), out_of_range&);
|
||||
// (the head of the array, 1, -3, 7, and 1.5 as a single-precision float)
|
||||
CHECK(out == std::vector<std::uint8_t>({0x86, 0x01, 0x22, 0x07, 0xFA, 0x3F, 0xC0, 0x00, 0x00}));
|
||||
out.clear();
|
||||
CHECK_THROWS_AS(wide_json::to_bon8(run, out), out_of_range&);
|
||||
CHECK(!out.empty());
|
||||
#endif
|
||||
|
||||
// the same values that fit are written exactly as with 64-bit number types
|
||||
const wide_json fits = wide_json::array({make_int(1), make_int(-3), make_uint(7), wide_json(1.5), make_int(min_int64), make_int(max_int64), make_int(200), make_int(-70000)});
|
||||
const nlohmann::json fits64 = nlohmann::json::array({1, -3, 7, 1.5, (std::numeric_limits<std::int64_t>::min)(), (std::numeric_limits<std::int64_t>::max)(), 200, -70000});
|
||||
CHECK(wide_json::to_cbor(fits) == nlohmann::json::to_cbor(fits64));
|
||||
CHECK(wide_json::to_msgpack(fits) == nlohmann::json::to_msgpack(fits64));
|
||||
CHECK(wide_json::to_ubjson(fits) == nlohmann::json::to_ubjson(fits64));
|
||||
CHECK(wide_json::to_ubjson(fits, true, true) == nlohmann::json::to_ubjson(fits64, true, true));
|
||||
CHECK(wide_json::to_bjdata(fits) == nlohmann::json::to_bjdata(fits64));
|
||||
CHECK(wide_json::to_bjdata(fits, true, true) == nlohmann::json::to_bjdata(fits64, true, true));
|
||||
CHECK(wide_json::to_bon8(fits) == nlohmann::json::to_bon8(fits64));
|
||||
CHECK(wide_json::to_bson(wrap(fits)) == nlohmann::json::to_bson(nlohmann::json::object({{"v", fits64}})));
|
||||
|
||||
const wide_json fit_members = wide_json::object({{"a", make_int(1)}, {"b", make_int(-300)}, {"c", make_uint(max_uint64)}, {"d", wide_json(2.5)}});
|
||||
const nlohmann::json fit_members64 = nlohmann::json::object({{"a", 1}, {"b", -300}, {"c", (std::numeric_limits<std::uint64_t>::max)()}, {"d", 2.5}});
|
||||
CHECK(wide_json::to_cbor(fit_members) == nlohmann::json::to_cbor(fit_members64));
|
||||
CHECK(wide_json::to_msgpack(fit_members) == nlohmann::json::to_msgpack(fit_members64));
|
||||
CHECK(wide_json::to_ubjson(fit_members) == nlohmann::json::to_ubjson(fit_members64));
|
||||
CHECK(wide_json::to_bjdata(fit_members) == nlohmann::json::to_bjdata(fit_members64));
|
||||
CHECK(wide_json::to_bson(fit_members) == nlohmann::json::to_bson(fit_members64));
|
||||
}
|
||||
|
||||
SECTION("BJData ND-array")
|
||||
{
|
||||
// an ND-array element beyond 64 bits does not fit any dtype, so the
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
using nlohmann::json;
|
||||
using nlohmann::ordered_json;
|
||||
using nlohmann::json_document;
|
||||
using nlohmann::ordered_json_editable_document;
|
||||
using nlohmann::json_view;
|
||||
using nlohmann::ordered_json_document;
|
||||
using nlohmann::ordered_json_view;
|
||||
@@ -219,6 +220,76 @@ TEST_CASE("json_view dump")
|
||||
CHECK(d.root()["a"].dump(-1, ' ', true).size() == 2 + (5000 * 6));
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with strings of the source, decoded strings, and edited strings")
|
||||
{
|
||||
// plain ASCII, non-ASCII, escaped (decoded), and mixed strings, as values and as keys;
|
||||
// the second text has a DEL, which ensure_ascii writes as an escape
|
||||
for (const char* const del :
|
||||
{"", "\x7F"
|
||||
})
|
||||
{
|
||||
const std::string text = std::string("{\"plain\":\"abc\",\"caf\xC3\xA9\":\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\",")
|
||||
+ "\"esc\\n\\\"\":\"a\\tb\\u00e4\\ud83d\\ude00\",\"del\":\"a" + del + "b\",\"\":\"\"," // NOLINT(modernize-raw-string-literal)
|
||||
+ "\"list\":[\"x\",\"\xC3\xBC\",\"\\\\\",\"long ASCII string with more than thirty-two bytes in it\","
|
||||
+ "\"long string with more than thirty-two bytes and one non-ASCII byte \xC3\xB6\"],"
|
||||
+ "\"n\":[1,2.5,-0,true,null]}";
|
||||
const ordered_json j = ordered_json::parse(text);
|
||||
const ordered_json_document d = ordered_json_document::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(4, ' ', true) == j.dump(4, ' ', true));
|
||||
CHECK(d.root().dump() == j.dump());
|
||||
CHECK(d.root()["list"].dump(-1, ' ', true) == j["list"].dump(-1, ' ', true));
|
||||
CHECK(d.root()["del"].dump(-1, ' ', true) == j["del"].dump(-1, ' ', true));
|
||||
|
||||
// strings that an edit wrote (the source strings that they replace were plain)
|
||||
ordered_json_editable_document e = ordered_json_editable_document::parse(text);
|
||||
ordered_json k = j;
|
||||
e.set(e.root()["plain"], std::string("\xC3\xA4gypten"));
|
||||
k["plain"] = "\xC3\xA4gypten";
|
||||
e.set(e.root()["list"][0], std::string("ascii after edit"));
|
||||
k["list"][0] = "ascii after edit";
|
||||
e.set(e.root()["list"][1], std::string("\xC3\xA4"));
|
||||
k["list"][1] = "\xC3\xA4";
|
||||
e.set(e.root()["del"], std::string("tab\there"));
|
||||
k["del"] = "tab\there";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
CHECK(e.root().dump(2, ' ', true) == k.dump(2, ' ', true));
|
||||
CHECK(e.root().dump() == k.dump());
|
||||
|
||||
// strings that were edited from or to ASCII text
|
||||
e.set(e.root()["list"][2], std::string("\xC3\xA4"));
|
||||
k["list"][2] = "\xC3\xA4";
|
||||
e.set(e.root()["list"][4], std::string("plain again"));
|
||||
k["list"][4] = "plain again";
|
||||
CHECK(e.root().dump(-1, ' ', true) == k.dump(-1, ' ', true));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("ensure_ascii with DEL or a non-ASCII character at every position")
|
||||
{
|
||||
// the check whether a string of the source has no byte >= 0x7F reads
|
||||
// eight bytes at a time and then the rest one by one
|
||||
for (std::size_t length = 1; length <= 20; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (const char* const c :
|
||||
{
|
||||
"\x7F", "\x7E", "\xC3\xA4", "\xC3\xBF" // NOLINT(modernize-raw-string-literal)
|
||||
})
|
||||
{
|
||||
const std::string s = std::string(pos, 'a') + c + std::string(length - pos - 1, 'b');
|
||||
std::string text = "[\"";
|
||||
text.append(s).append("\",{\"").append(s).append("\":1}]");
|
||||
const json_document d = json_document::parse(text);
|
||||
const json j = json::parse(text);
|
||||
CHECK(d.root().dump(-1, ' ', true) == j.dump(-1, ' ', true));
|
||||
CHECK(d.root().dump(1, ' ', true) == j.dump(1, ' ', true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("streams and discarded views")
|
||||
{
|
||||
const json_document d = json_document::parse(R"({"a": [1, 2]})");
|
||||
|
||||
@@ -1417,6 +1417,27 @@ void check_dump_against_reference(const std::string& s, const bool with_strict =
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("every ASCII character at every position of a short string is written like the reference")
|
||||
{
|
||||
// dump_escaped() copies strings of up to 32 bytes of printable ASCII after
|
||||
// a table lookup per byte, and hands everything else to the scanners; put
|
||||
// each ASCII character and a 2-byte character at every position of
|
||||
// strings of up to 40 bytes to cover both sides of that bound
|
||||
for (std::size_t length = 1; length <= 40; ++length)
|
||||
{
|
||||
for (std::size_t pos = 0; pos < length; ++pos)
|
||||
{
|
||||
for (int c = 0; c < 0x80; ++c)
|
||||
{
|
||||
std::string s(length, 'a');
|
||||
s[pos] = static_cast<char>(c);
|
||||
check_dump_against_reference(s);
|
||||
}
|
||||
check_dump_against_reference(std::string(pos, 'a') + "\xC3\xA4" + std::string(length - pos - 1, 'a'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("invalid UTF-8 at every position is handled like the reference")
|
||||
{
|
||||
// ill-formed byte sequences; those that end in a truncated sequence are
|
||||
@@ -1559,3 +1580,94 @@ TEST_CASE("invalid UTF-8 at every position is handled like the reference")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("short strings are written like the reference, as values and as keys")
|
||||
{
|
||||
// strings of up to 16 characters (32 with vector instructions) that need no
|
||||
// escaping are written as a whole (together with the quotes and, for a key,
|
||||
// the colon); everything else, including a string that is merely short,
|
||||
// takes the general path.
|
||||
// Every length around the edges of the size classes of the fast path is
|
||||
// combined with each kind of byte that must send a string to the general
|
||||
// path, at each position, and compared with the reference for values and,
|
||||
// via the value, for keys.
|
||||
const std::vector<std::string> specials =
|
||||
{
|
||||
"\"", "\\", "\n", "\x1f", "\x7f", "\xc3\xa9", "\xe2\x82\xac", "\xf0\x9f\x98\x80", "\xff", "\x80", "\xc3"
|
||||
};
|
||||
|
||||
for (std::size_t length = 0; length <= 36; ++length)
|
||||
{
|
||||
const std::string plain(length, 'k');
|
||||
check_dump_against_reference(plain, false);
|
||||
|
||||
for (const auto& special : specials)
|
||||
{
|
||||
for (std::size_t pos = 0; pos <= length; ++pos)
|
||||
{
|
||||
std::string s = plain;
|
||||
s.insert(pos, special);
|
||||
check_dump_against_reference(s, false);
|
||||
|
||||
for (const bool ensure_ascii :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
// a key and a value are written alike, in compact and
|
||||
// in pretty-printed form
|
||||
const json value = s;
|
||||
const json object = json::object({{s, 1}});
|
||||
for (const auto error_handler :
|
||||
{
|
||||
json::error_handler_t::replace, json::error_handler_t::keep
|
||||
})
|
||||
{
|
||||
const std::string quoted = value.dump(-1, ' ', ensure_ascii, error_handler);
|
||||
CHECK(object.dump(-1, ' ', ensure_ascii, error_handler) == "{" + quoted + ":1}");
|
||||
CHECK(object.dump(2, ' ', ensure_ascii, error_handler) == "{\n " + quoted + ": 1\n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("runs of multi-byte characters are escaped like the reference under ensure_ascii")
|
||||
{
|
||||
// a run of well-formed multi-byte characters is escaped straight into the
|
||||
// write buffer; it has to flush that mid-run, stop at an ASCII character or
|
||||
// an ill-formed sequence and leave those to the general path, and pick up
|
||||
// behind what the general path has pending
|
||||
const std::vector<std::string> characters =
|
||||
{
|
||||
"\xc2\x80", "\xdf\xbf", "\xe0\xa0\x80", "\xe2\x82\xac", "\xed\x9f\xbf", "\xef\xbf\xbf",
|
||||
"\xf0\x90\x80\x80", "\xf0\x9f\x98\x80", "\xf4\x8f\xbf\xbf"
|
||||
};
|
||||
const std::vector<std::string> interruptions =
|
||||
{
|
||||
"", "a", "\n", "\"", "\x7f", "\xff", "\x80", "\xc3", "\xe2\x82", "\xf0\x9f\x98", "\xed\xa0\x80"
|
||||
};
|
||||
|
||||
for (std::size_t shift = 0; shift <= 13; ++shift)
|
||||
{
|
||||
for (const auto& interruption : interruptions)
|
||||
{
|
||||
for (const std::size_t count :
|
||||
{
|
||||
std::size_t{1}, std::size_t{2}, std::size_t{85}, std::size_t{200}
|
||||
})
|
||||
{
|
||||
std::string s(shift, 'x');
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
s += characters[(i + shift) % characters.size()];
|
||||
}
|
||||
s += interruption;
|
||||
s += characters[shift % characters.size()];
|
||||
s += characters[(shift + 3) % characters.size()];
|
||||
check_dump_against_reference(s, shift < 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
// The detail helpers that stand in for standard library functions of newer
|
||||
// standards: each uses the standard function where the library has it and a
|
||||
// C++11 implementation otherwise, so this file is also built with
|
||||
// JSON_HAS_CPP_17, JSON_HAS_CPP_20, and JSON_HAS_CPP_23 to test both.
|
||||
|
||||
namespace
|
||||
{
|
||||
struct with_constexpr_member
|
||||
{
|
||||
static constexpr std::size_t lower = 64; // no out-of-line definition
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("detail::clamp")
|
||||
{
|
||||
using nlohmann::detail::clamp;
|
||||
|
||||
SECTION("integers")
|
||||
{
|
||||
CHECK(clamp(5, 1, 10) == 5);
|
||||
CHECK(clamp(0, 1, 10) == 1);
|
||||
CHECK(clamp(11, 1, 10) == 10);
|
||||
CHECK(clamp(1, 1, 10) == 1);
|
||||
CHECK(clamp(10, 1, 10) == 10);
|
||||
CHECK(clamp(7, 7, 7) == 7);
|
||||
CHECK(clamp(-5, -3, 3) == -3);
|
||||
CHECK(clamp(std::size_t{0}, std::size_t{64}, std::size_t{0x10000}) == 64);
|
||||
CHECK(clamp(std::size_t{0x20000}, std::size_t{64}, std::size_t{0x10000}) == 0x10000);
|
||||
}
|
||||
|
||||
SECTION("floating-point numbers")
|
||||
{
|
||||
CHECK(clamp(0.5, 0.0, 1.0) == 0.5);
|
||||
CHECK(clamp(-0.5, 0.0, 1.0) == 0.0);
|
||||
CHECK(clamp(1.5, 0.0, 1.0) == 1.0);
|
||||
CHECK(clamp(-std::numeric_limits<double>::infinity(), 0.0, 1.0) == 0.0);
|
||||
}
|
||||
|
||||
SECTION("by value: a static constexpr member needs no definition")
|
||||
{
|
||||
CHECK(clamp(std::size_t{1}, with_constexpr_member::lower, std::size_t{100}) == 64);
|
||||
}
|
||||
|
||||
SECTION("constexpr and noexcept")
|
||||
{
|
||||
static_assert(clamp(3, 1, 2) == 2, "clamp must be constexpr");
|
||||
static_assert(noexcept(clamp(3, 1, 2)), "clamp must be noexcept");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("detail::bit_cast")
|
||||
{
|
||||
using nlohmann::detail::bit_cast;
|
||||
|
||||
CHECK(bit_cast<std::uint64_t>(1.0) == 0x3FF0000000000000u);
|
||||
CHECK(bit_cast<std::uint64_t>(-2.0) == 0xC000000000000000u);
|
||||
CHECK(bit_cast<std::uint32_t>(1.0f) == 0x3F800000u);
|
||||
CHECK(bit_cast<double>(std::uint64_t{0x4009000000000000u}) == 3.125);
|
||||
CHECK(bit_cast<float>(std::uint32_t{0xC0490000u}) == -3.140625f);
|
||||
CHECK(bit_cast<std::int8_t>(std::uint8_t{0xFF}) == -1);
|
||||
|
||||
// to and from byte arrays
|
||||
const auto bytes = bit_cast<std::array<std::uint8_t, 4>>(std::uint32_t{0x01020304u});
|
||||
// qualified: with a std::array argument, ADL would also find std::bit_cast in C++20
|
||||
CHECK(nlohmann::detail::bit_cast<std::uint32_t>(bytes) == 0x01020304u);
|
||||
|
||||
static_assert(noexcept(bit_cast<std::uint64_t>(1.0)), "bit_cast must be noexcept");
|
||||
}
|
||||
|
||||
TEST_CASE("detail::byteswap")
|
||||
{
|
||||
using nlohmann::detail::byteswap;
|
||||
|
||||
CHECK(byteswap(std::uint16_t{0x0102u}) == 0x0201u);
|
||||
CHECK(byteswap(std::uint16_t{0xFF00u}) == 0x00FFu);
|
||||
CHECK(byteswap(std::uint32_t{0x01020304u}) == 0x04030201u);
|
||||
CHECK(byteswap(std::uint32_t{0xFF000080u}) == 0x800000FFu);
|
||||
CHECK(byteswap(std::uint64_t{0x0102030405060708u}) == 0x0807060504030201u);
|
||||
CHECK(byteswap(std::uint64_t{0xFF00000000000080u}) == 0x80000000000000FFu);
|
||||
|
||||
// an involution
|
||||
for (std::uint64_t x :
|
||||
{
|
||||
std::uint64_t{0}, std::uint64_t{1}, std::uint64_t{0x123456789ABCDEF0u}, (std::numeric_limits<std::uint64_t>::max)()
|
||||
})
|
||||
{
|
||||
CHECK(byteswap(byteswap(x)) == x);
|
||||
CHECK(byteswap(byteswap(static_cast<std::uint32_t>(x))) == static_cast<std::uint32_t>(x));
|
||||
CHECK(byteswap(byteswap(static_cast<std::uint16_t>(x))) == static_cast<std::uint16_t>(x));
|
||||
}
|
||||
|
||||
static_assert(std::is_same < decltype(byteswap(std::uint16_t{})), std::uint16_t >::value, "byteswap keeps the type");
|
||||
static_assert(std::is_same < decltype(byteswap(std::uint32_t{})), std::uint32_t >::value, "byteswap keeps the type");
|
||||
static_assert(std::is_same < decltype(byteswap(std::uint64_t{})), std::uint64_t >::value, "byteswap keeps the type");
|
||||
}
|
||||
|
||||
TEST_CASE("detail::count_leading_zeros and count_trailing_zeros")
|
||||
{
|
||||
using nlohmann::detail::count_leading_zeros;
|
||||
using nlohmann::detail::count_trailing_zeros;
|
||||
|
||||
// a single bit, and that bit with all lower (resp. higher) bits set: both
|
||||
// halves of the 32-bit fallbacks
|
||||
for (int k = 0; k < 64; ++k)
|
||||
{
|
||||
const std::uint64_t bit = std::uint64_t{1} << k;
|
||||
CAPTURE(k)
|
||||
CHECK(count_leading_zeros(bit) == 63 - k);
|
||||
CHECK(count_leading_zeros(bit | (bit - 1)) == 63 - k);
|
||||
CHECK(count_trailing_zeros(bit) == k);
|
||||
CHECK(count_trailing_zeros(bit | ~(bit | (bit - 1))) == k);
|
||||
}
|
||||
}
|
||||
@@ -48,14 +48,16 @@ namespace
|
||||
class chunked_streambuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
chunked_streambuf(std::string s, std::size_t chunk_size)
|
||||
: m_data(std::move(s)), m_chunk_size(chunk_size)
|
||||
// The first @a unbuffered characters are served without a get area, the
|
||||
// rest in get areas of @a chunk_size.
|
||||
chunked_streambuf(std::string s, std::size_t chunk_size, std::size_t unbuffered = 0)
|
||||
: m_data(std::move(s)), m_chunk_size(chunk_size), m_unbuffered(unbuffered)
|
||||
{}
|
||||
|
||||
protected:
|
||||
int_type underflow() override
|
||||
{
|
||||
if (m_chunk_size == 0)
|
||||
if (m_chunk_size == 0 || m_pos < m_unbuffered)
|
||||
{
|
||||
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos]) : traits_type::eof();
|
||||
}
|
||||
@@ -77,7 +79,7 @@ class chunked_streambuf : public std::streambuf
|
||||
|
||||
int_type uflow() override
|
||||
{
|
||||
if (m_chunk_size == 0)
|
||||
if (m_chunk_size == 0 || m_pos < m_unbuffered)
|
||||
{
|
||||
return m_pos < m_data.size() ? traits_type::to_int_type(m_data[m_pos++]) : traits_type::eof();
|
||||
}
|
||||
@@ -88,6 +90,7 @@ class chunked_streambuf : public std::streambuf
|
||||
std::string m_data;
|
||||
std::size_t m_pos = 0;
|
||||
std::size_t m_chunk_size;
|
||||
std::size_t m_unbuffered;
|
||||
};
|
||||
|
||||
// how a test reads from the stream
|
||||
@@ -101,9 +104,9 @@ enum class read_mode
|
||||
// Read @a input from a chunked_streambuf and describe everything observable:
|
||||
// the values or the exception, the stream state after each value, and the
|
||||
// characters left in the stream.
|
||||
std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode)
|
||||
std::string observe(const std::string& input, std::size_t chunk_size, read_mode mode, std::size_t unbuffered = 0)
|
||||
{
|
||||
chunked_streambuf buf(input, chunk_size);
|
||||
chunked_streambuf buf(input, chunk_size, unbuffered);
|
||||
std::istream is(&buf);
|
||||
std::string result;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
@@ -171,7 +174,8 @@ std::vector<std::string> corpus()
|
||||
R"("unterminated)", "\"control\x01character\"", "\"ill-formed \xc3\x28 UTF-8\"", R"("bad \x escape")",
|
||||
R"("\ud800 lone surrogate")",
|
||||
// literals
|
||||
"true", "false", "null", "tru", "nul", "falsey",
|
||||
"true", "false", "null", "tru", "nul", "falsey", "[true,false,null]", "nulL", "truex", "[fals e]", "t",
|
||||
"true1", "null null", "[false]",
|
||||
// structures
|
||||
"[]", "{}", "[1,2,3]", R"({"a":1,"b":[true,false,null],"c":{"d":"e"}})", "[1,]", R"({"a" 1})", "[[[[[]]]]]",
|
||||
// pretty-printed
|
||||
@@ -211,6 +215,31 @@ TEST_CASE("parsing from a std::istream scans the get area in bulk")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("results do not depend on a get area that appears late")
|
||||
{
|
||||
// the first characters come without a get area, then the streambuf
|
||||
// starts to use one: the token that is read at that point is complete
|
||||
for (const auto& input : corpus())
|
||||
{
|
||||
const std::string expected = observe(input, 0, read_mode::extract);
|
||||
for (const std::size_t unbuffered :
|
||||
{
|
||||
1u, 2u, 3u, 5u, 8u, 13u, 21u
|
||||
})
|
||||
{
|
||||
for (const std::size_t chunk_size :
|
||||
{
|
||||
1u, 2u, 3u, 7u, 64u
|
||||
})
|
||||
{
|
||||
CAPTURE(input)
|
||||
CAPTURE(unbuffered)
|
||||
CAPTURE(chunk_size)
|
||||
CHECK(observe(input, chunk_size, read_mode::extract, unbuffered) == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // !defined(JSON_NOEXCEPTION)
|
||||
|
||||
SECTION("values are the same as when parsing from a string")
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
using nlohmann::detail::dtoa_impl::reinterpret_bits;
|
||||
using nlohmann::detail::bit_cast;
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -44,7 +44,7 @@ float make_float(uint32_t sign_bit, uint32_t biased_exponent, uint32_t significa
|
||||
bits |= biased_exponent << 23;
|
||||
bits |= significand;
|
||||
|
||||
return reinterpret_bits<float>(bits);
|
||||
return bit_cast<float>(bits);
|
||||
}
|
||||
|
||||
// ldexp -- convert f * 2^e to IEEE single precision
|
||||
@@ -81,7 +81,7 @@ float make_float(uint64_t f, int e)
|
||||
: static_cast<uint64_t>(e + kExponentBias);
|
||||
|
||||
const uint64_t bits = (f & kSignificandMask) | (biased_exponent << kPhysicalSignificandSize);
|
||||
return reinterpret_bits<float>(static_cast<uint32_t>(bits));
|
||||
return bit_cast<float>(static_cast<uint32_t>(bits));
|
||||
}
|
||||
|
||||
double make_double(uint64_t sign_bit, uint64_t biased_exponent, uint64_t significand)
|
||||
@@ -96,7 +96,7 @@ double make_double(uint64_t sign_bit, uint64_t biased_exponent, uint64_t signifi
|
||||
bits |= biased_exponent << 52;
|
||||
bits |= significand;
|
||||
|
||||
return reinterpret_bits<double>(bits);
|
||||
return bit_cast<double>(bits);
|
||||
}
|
||||
|
||||
// ldexp -- convert f * 2^e to IEEE double precision
|
||||
@@ -133,7 +133,7 @@ double make_double(uint64_t f, int e)
|
||||
: static_cast<uint64_t>(e + kExponentBias);
|
||||
|
||||
const uint64_t bits = (f & kSignificandMask) | (biased_exponent << kPhysicalSignificandSize);
|
||||
return reinterpret_bits<double>(bits);
|
||||
return bit_cast<double>(bits);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -652,7 +652,7 @@ bool reads_back(const std::string& digits, int e, double v)
|
||||
{
|
||||
const std::string text = digits + "e" + std::to_string(e);
|
||||
// (compared bit for bit: v is positive and finite, and -Wfloat-equal)
|
||||
return reinterpret_bits<std::uint64_t>(parse_double(text)) == reinterpret_bits<std::uint64_t>(v);
|
||||
return bit_cast<std::uint64_t>(parse_double(text)) == bit_cast<std::uint64_t>(v);
|
||||
}
|
||||
|
||||
/// Check the representation of a positive finite double: it reads back as
|
||||
@@ -665,7 +665,7 @@ void check_shortest(double v)
|
||||
CAPTURE(text)
|
||||
CHECK(parse_double(text) == v);
|
||||
// the layout is that of format_buffer() for the digits of Zmij
|
||||
const auto sd = nlohmann::detail::zmij::to_shortest(reinterpret_bits<std::uint64_t>(v));
|
||||
const auto sd = nlohmann::detail::zmij::to_shortest(bit_cast<std::uint64_t>(v));
|
||||
const std::uint64_t significand = sd.has_digit ? (sd.integral * 10) + sd.digit : sd.integral;
|
||||
int exponent = sd.has_digit ? sd.exponent : sd.exponent + 1;
|
||||
std::string significand_digits = std::to_string(significand);
|
||||
@@ -779,7 +779,7 @@ TEST_CASE("shortest digits of doubles")
|
||||
for (int i = 0; i < 100000; ++i)
|
||||
{
|
||||
const std::uint64_t bits = rng() & 0x7FFFFFFFFFFFFFFFu;
|
||||
const auto v = reinterpret_bits<double>(bits);
|
||||
const auto v = bit_cast<double>(bits);
|
||||
if (std::isfinite(v) && bits != 0)
|
||||
{
|
||||
check_shortest(v);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# astyle is deliberately pinned: newer versions reformat the code, and this version
|
||||
# defines the formatting that `make amalgamate` produces and check_amalgamation.yml enforces.
|
||||
# Dependabot is told to ignore it in .github/dependabot.yml.
|
||||
astyle==3.4.13
|
||||
@@ -1,4 +1,62 @@
|
||||
# astyle is deliberately pinned: newer versions reformat the code, and this version
|
||||
# defines the formatting that `make amalgamate` produces and check_amalgamation.yml enforces.
|
||||
# Dependabot is told to ignore it in .github/dependabot.yml.
|
||||
astyle==3.4.13
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.14
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --no-emit-index-url --output-file=tools/astyle/requirements.txt --strip-extras tools/astyle/requirements.in
|
||||
#
|
||||
astyle==3.4.13 \
|
||||
--hash=sha256:0456c5bb1bed18b30f7d9e9c7d6499684a62cc808949173d52bf6def655f48b9 \
|
||||
--hash=sha256:061b320ea1d265ac01babdd82b056c7b55913c726c770cff26191af78884c5ab \
|
||||
--hash=sha256:0ad050135504994bee50233db59bedf95f1343977118e27f495eaf2f401ecfe5 \
|
||||
--hash=sha256:162cb29abc5ddd5d03ab48b15fd8603baf04c1ba5913e1ef475e09818744216b \
|
||||
--hash=sha256:181d7b6da8eef2cc4dc757dc3381f0d1f2932d6ceb2f6ce5bea432de698e9a8f \
|
||||
--hash=sha256:191abde742ff5ae788a489722a669f7bc0b18cefc1020b617bc0e84361b71a58 \
|
||||
--hash=sha256:1bf59aa3006546d795443d1ff69682f989fe95e4c71850c67f8251eab183fee3 \
|
||||
--hash=sha256:31c86eb8aae517b9cf1debd325ea5f6ac653d52787d5c5c9d56197afc9bc97e3 \
|
||||
--hash=sha256:35b979a62d1499b2daf241517ddb0aee3ab4b82dc82aa3f8604a2db1e9cc4e66 \
|
||||
--hash=sha256:36a9f14f88c6bc3c4240f977a3f3fd3d0eac84373bc10004258712e6b3cb08f2 \
|
||||
--hash=sha256:46ad33bf795e68f4b18bf947cc7276e26579ab6df89574617c7797b0ed2cf473 \
|
||||
--hash=sha256:4803a16330b67b98c994b87b984af00d7dcaff0d4af69d17ab0fd705141725fe \
|
||||
--hash=sha256:481b6e195dfa56380bb19cd134dc698cd7f5732d7d5cd781c13b5c15112b8fc2 \
|
||||
--hash=sha256:48f0218622e807360d50d9d419e527d8e4c0f7f49c51c6fac3d4c6f343a0573e \
|
||||
--hash=sha256:4a3179783910ea386125d88a16e86cc6cfee41c513d0d8e692bdabd17577ce93 \
|
||||
--hash=sha256:508d5debe7874a5dd1b491bbae0c48ebc94aa3736545718208bb33c648962e62 \
|
||||
--hash=sha256:55b4335f20b717f6ec57a237d29246e3c7196bd014e509f2aa376aca36ea9967 \
|
||||
--hash=sha256:56714bb2e2a0fe3911474b66e563aac508d97a9f48d5e55c8e182b1675db0e05 \
|
||||
--hash=sha256:599ab574cd53b7105877d5e1b6ab0c933b771e4b2343b0bc54aa06af64653449 \
|
||||
--hash=sha256:5b183422a1edbce971758b2b4a2b180127d9e8c46119183d410c77ec1d42a516 \
|
||||
--hash=sha256:5c6de6933f7818b4380b309704c45da9330d906bbe479bb76023261e63545900 \
|
||||
--hash=sha256:5f7efd14d9aaee69a4a33e60722a8a09d0b7cb834aed698ce0793cecec4e7452 \
|
||||
--hash=sha256:66ecd8a2a778bc78bdb2df4f5f371f100c6f180ae859e7d414f374eb2636b531 \
|
||||
--hash=sha256:6fa1c231c82a0ea6ccfa8e19276dec4753d9e084229d606f9522260fe37b955f \
|
||||
--hash=sha256:76d53ee9e07f8ad32050363c8402923095020856c44015968b4dd9ea5b7a18a1 \
|
||||
--hash=sha256:81f0173912da9cf687cf2ed1361026fd957fcfd5b9673803c50265db63427d7f \
|
||||
--hash=sha256:81f4345adbbb463a2d5ce314a1ca689a0aa05a6240c86fc0225e92474e3f44b4 \
|
||||
--hash=sha256:840e1d0436a1a3793ebe14c95fefe797f77bd78d6194a7c0bcdde705cb583403 \
|
||||
--hash=sha256:8d6a69f9b316b3941cd43f16eb109f738bb171963408b5f135521ef70ad364f1 \
|
||||
--hash=sha256:8e2879d770e3fbf53ff62728f4d62a6edcacd1ed3d62b36c012706d778048340 \
|
||||
--hash=sha256:900e442ed002e5e05c31628fac817db62f6d28e1cc2426040c69a10d39f92651 \
|
||||
--hash=sha256:920c8516f420da68765ced84692e550d0f34932663219c48db9b0c79544002d3 \
|
||||
--hash=sha256:9417573994a9d264f873cd6122f9e0ea7044d628d9a8b9e84dbbc85b0e7c2880 \
|
||||
--hash=sha256:95eaf11aadd770bccd833c8ffe222ea6055fe5c2c4b3974cb43f3366913b62a4 \
|
||||
--hash=sha256:9b7471f4a5f174604cdfdf4f9091ba319edb52471e07672ea71745157d121fe7 \
|
||||
--hash=sha256:9e016b099f809c8d4894fa6d36ef925973f52ebc50136306b1b4b9ce65094063 \
|
||||
--hash=sha256:9e04dbd51f564316a4879448258d9b9e68ade9b062351ea6fc7356d0f42471a0 \
|
||||
--hash=sha256:a002a3a1ac187ba1b441f678aca16b4ecea47a1f3d2462c3ec9c59e71e6083b7 \
|
||||
--hash=sha256:a177039c38f6f6a43f46bcd3571756be385c0c47462e6a06eb5950397d0bdcbe \
|
||||
--hash=sha256:a8963c261b980a0c12fc1523f86eab3edc67940c114ea544b21d2b44e9196d23 \
|
||||
--hash=sha256:a97f8ef2a054ec067e3af65bc5b145c51b6c2a5f2efe997a72a9d958c3ab6d1a \
|
||||
--hash=sha256:aa8ba72c1349b7e45d1ce01d69daecfac680713624d27d83ebf2a9d1b4984f65 \
|
||||
--hash=sha256:abba627fb930a5398414ba398169f52e3865855501b5ee5430bb53d0974074c6 \
|
||||
--hash=sha256:ac8db98b392b609f36cd7d6e7f18a247376723b4397578a6f450a445a9dacb3d \
|
||||
--hash=sha256:c9d4b1a0a7fbc35b96273695143c791df9c1a4dcab0815ae6d9db8483314430f \
|
||||
--hash=sha256:cf247a3bca29f43398af4e20c7c922b7f5130a22f306f068ad038b94e72854d6 \
|
||||
--hash=sha256:d3da892b9ec2932a2cb49941aae79447f6baafe5023ee31680c9cabcc471b451 \
|
||||
--hash=sha256:db8c4555b66067aba253c2f32dc4ff6ee92882dd3e95a00ebb3bbf145efe067d \
|
||||
--hash=sha256:dde61d5acb6008ebefcecc37625b7f2776df2f368f92d47714e5f50276347c9f \
|
||||
--hash=sha256:dede5398c41b3a00ad0a9f77c57c05444640f8b28500b9bb4624ab6f2d0a90b7 \
|
||||
--hash=sha256:e3cc45784dbac7ade37967e24cbd6b5b89517f3df8fbed276dffffdcf289f04d \
|
||||
--hash=sha256:eb4d38f4a6be78cd28ce44397a1244c08af15c1a00293c40634c2104b44c943c \
|
||||
--hash=sha256:f9e6cd9ee60a8c42eab85cb57e0c82b1c947261b71f5091bbb9f61e864c64e07 \
|
||||
--hash=sha256:fb1ddc816b483c83de15db98847a5d2dd3ba9df9f4a937ad47aabadeeaeed58c
|
||||
# via -r tools/astyle/requirements.in
|
||||
@@ -1,6 +1,10 @@
|
||||
# generate_natvis.py
|
||||
|
||||
Generate the Natvis debugger visualization file for all supported namespace combinations.
|
||||
Generate the Natvis debugger visualization file for namespace combinations with up to two ABI tags.
|
||||
|
||||
The checked-in `nlohmann_json.natvis` covers the default namespace (no ABI tags), every single ABI tag, and every pair
|
||||
of ABI tags, with both versioned and unversioned namespace variants. Configurations that enable three or more ABI tags
|
||||
are not covered by this reduced file; use `--abi-tags` or `--all` (see below) to generate a file for them.
|
||||
|
||||
The ABI tag list and the library version are parsed from
|
||||
`include/nlohmann/detail/abi_macros.hpp`, so this script must be re-run (via
|
||||
@@ -19,10 +23,43 @@ make natvis
|
||||
`requirements.txt` (Jinja2) and runs the script with it. Alternatively, install the requirements yourself and run:
|
||||
|
||||
```shell
|
||||
./generate_natvis.py [--version X.Y.Z] [repository_root/]
|
||||
./generate_natvis.py [--version X.Y.Z] [--abi-tags TAGS]... [--all] [--list-tags] [--output FILE] [repository_root/]
|
||||
```
|
||||
|
||||
`--version` and the output/repository-root directory both default to values
|
||||
`--version` and the repository root both default to values
|
||||
derived from this script's own location, so they only need to be given
|
||||
explicitly when generating a Natvis file for a different checkout or a
|
||||
version other than the one in `abi_macros.hpp`.
|
||||
|
||||
| Option | Description |
|
||||
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--list-tags` | Print every ABI tag (in namespace order) with the macro that enables it and the `NLOHMANN_JSON_ABI_TAG_*` identifier, then exit. |
|
||||
| `--abi-tags TAGS` | Add the namespaces of one configuration, given as a comma-separated list of ABI tags (`_diag,_dp,_vns`; the leading underscore is optional, and the order does not matter). Can be repeated for several configurations. Unknown tags are an error. |
|
||||
| `--all` | Generate the namespaces for every combination of ABI tags. The file size doubles with each tag (about 60 MB for 12 tags). Cannot be combined with `--abi-tags`. |
|
||||
| `--output FILE` | Write the Natvis file to `FILE` instead of `nlohmann_json.natvis` in the repository root. |
|
||||
|
||||
Each configuration gets both the versioned (`json_abi_diag_dp_vns_v3_12_0`) and the unversioned (`json_abi_diag_dp_vns`,
|
||||
used with `NLOHMANN_JSON_NAMESPACE_NO_VERSION`) namespace. The default namespaces (up to two ABI tags) are always
|
||||
included, so a generated file can replace the checked-in one.
|
||||
|
||||
### Generating a Natvis file for your configuration
|
||||
|
||||
1. Check out the release tag of the library version you use (the ABI tags and the version are read from the checkout).
|
||||
2. Find the ABI tags of the macros you enable:
|
||||
|
||||
```shell
|
||||
make natvis NATVIS_ARGS="--list-tags"
|
||||
```
|
||||
|
||||
3. Generate a Natvis file with them, for example for `JSON_DIAGNOSTICS`, `JSON_DIAGNOSTIC_POSITIONS`, and
|
||||
`JSON_VIEW_NO_SIMD`:
|
||||
|
||||
```shell
|
||||
make natvis NATVIS_ARGS="--abi-tags _diag,_dp,_vns --output my_nlohmann_json.natvis"
|
||||
```
|
||||
|
||||
4. Use `my_nlohmann_json.natvis` instead of the shipped `nlohmann_json.natvis`, for example by adding it to your Visual
|
||||
Studio project.
|
||||
|
||||
Passing `--output` keeps the checked-in `nlohmann_json.natvis` unchanged; without it, `make check-amalgamation` would
|
||||
report the file as out of date.
|
||||
@@ -20,9 +20,11 @@ def semver(v):
|
||||
|
||||
|
||||
def abi_info(repo_root):
|
||||
"""Derive the ABI tag list (in NLOHMANN_JSON_ABI_TAGS_CONCAT order) and the
|
||||
"""Derive the ABI tags (in NLOHMANN_JSON_ABI_TAGS_CONCAT order) and the
|
||||
library version from <repo_root>/include/nlohmann/detail/abi_macros.hpp,
|
||||
so this script cannot drift from the header it visualizes."""
|
||||
so this script cannot drift from the header it visualizes. Each tag is
|
||||
returned as a (suffix, configuration macro, tag macro) tuple, for example
|
||||
('_diag', 'JSON_DIAGNOSTICS', 'NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS')."""
|
||||
abi_macros_hpp = os.path.join(repo_root, 'include', 'nlohmann', 'detail', 'abi_macros.hpp')
|
||||
with open(abi_macros_hpp) as f:
|
||||
content = f.read()
|
||||
@@ -47,7 +49,9 @@ def abi_info(repo_root):
|
||||
match = re.search(r'#define\s+' + re.escape(ident) + r'\s+(_\w+)\s*\n', content)
|
||||
if not match:
|
||||
raise ValueError(f'could not find a non-empty #define for {ident} in {abi_macros_hpp}')
|
||||
abi_tags.append(match.group(1))
|
||||
# the option that enables the tag is the #if/#ifdef right above it
|
||||
option = re.search(r'#\s*(?:if|ifdef)\s+(\w+)\s*\n\s*#define\s+' + re.escape(ident) + r'\s+_', content)
|
||||
abi_tags.append((match.group(1), option.group(1) if option else '?', ident))
|
||||
|
||||
version = {}
|
||||
for part in ('MAJOR', 'MINOR', 'PATCH'):
|
||||
@@ -59,30 +63,79 @@ def abi_info(repo_root):
|
||||
return abi_tags, '{MAJOR}.{MINOR}.{PATCH}'.format(**version)
|
||||
|
||||
|
||||
def parse_tag_list(value, abi_tags):
|
||||
"""Turn one --abi-tags value ("_diag,dp") into the tuple of suffixes it
|
||||
names, in namespace order; unknown tags and empty lists are errors."""
|
||||
known = [suffix for suffix, _, _ in abi_tags]
|
||||
requested = set()
|
||||
for tag in value.split(','):
|
||||
tag = tag.strip()
|
||||
if not tag:
|
||||
continue
|
||||
if not tag.startswith('_'):
|
||||
tag = '_' + tag
|
||||
if tag not in known:
|
||||
raise ValueError(f'unknown ABI tag {tag!r}; valid tags: {", ".join(known)} (see --list-tags)')
|
||||
requested.add(tag)
|
||||
if not requested:
|
||||
raise ValueError(f'empty ABI tag list {value!r}; the namespace without tags is always generated')
|
||||
return tuple(tag for tag in known if tag in requested)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate nlohmann_json.natvis. By default, it covers the namespaces with up to two ABI tags; '
|
||||
'use --abi-tags or --all for other configurations.')
|
||||
parser.add_argument('--version', type=semver,
|
||||
help='Library version number (default: parsed from '
|
||||
'include/nlohmann/detail/abi_macros.hpp below "output")')
|
||||
parser.add_argument('output', nargs='?', default=REPO_ROOT,
|
||||
help='Repository root: where include/nlohmann/detail/abi_macros.hpp is '
|
||||
'read from and where nlohmann_json.natvis is written '
|
||||
'include/nlohmann/detail/abi_macros.hpp below "repo_root")')
|
||||
selection = parser.add_mutually_exclusive_group()
|
||||
selection.add_argument('--abi-tags', action='append', default=[], metavar='TAGS',
|
||||
help='Comma-separated list of ABI tags (e.g. "_diag,_dp,_vns", leading underscores are '
|
||||
'optional) of one configuration to add to the default namespaces; can be repeated')
|
||||
selection.add_argument('--all', action='store_true',
|
||||
help='Generate the namespaces for every combination of ABI tags; the file size doubles '
|
||||
'with each ABI tag (about 60 MB for 12 tags)')
|
||||
parser.add_argument('--list-tags', action='store_true',
|
||||
help='List the ABI tags with the macros that enable them, and exit')
|
||||
parser.add_argument('--output', metavar='FILE',
|
||||
help='Where to write the Natvis file (default: nlohmann_json.natvis below "repo_root")')
|
||||
parser.add_argument('repo_root', nargs='?', default=REPO_ROOT,
|
||||
help='Repository root: where include/nlohmann/detail/abi_macros.hpp is read from '
|
||||
'(default: the repository root this script lives in)')
|
||||
args = parser.parse_args()
|
||||
|
||||
derived_tags, derived_version = abi_info(args.output)
|
||||
abi_tags, derived_version = abi_info(args.repo_root)
|
||||
|
||||
if args.list_tags:
|
||||
width = max(len(suffix) for suffix, _, _ in abi_tags)
|
||||
option_width = max(len(option) for _, option, _ in abi_tags)
|
||||
for suffix, option, ident in abi_tags:
|
||||
print(f'{suffix:<{width}} {option:<{option_width}} {ident}')
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
extra_combinations = [parse_tag_list(value, abi_tags) for value in args.abi_tags]
|
||||
except ValueError as e:
|
||||
parser.error(str(e))
|
||||
|
||||
namespaces = ['nlohmann']
|
||||
abi_prefix = 'json_abi'
|
||||
abi_tags = derived_tags
|
||||
suffixes = [suffix for suffix, _, _ in abi_tags]
|
||||
version = '_v' + (args.version or derived_version).replace('.', '_')
|
||||
inline_namespaces = []
|
||||
|
||||
# generate all combinations of inline namespace names
|
||||
for n in range(0, len(abi_tags) + 1):
|
||||
for tags in itertools.combinations(abi_tags, n):
|
||||
ns = abi_prefix + ''.join(tags)
|
||||
inline_namespaces += [ns, ns + version]
|
||||
# by default, generate the namespace names for up to two ABI tags, plus
|
||||
# the combinations requested with --abi-tags
|
||||
max_tags = len(suffixes) if args.all else min(len(suffixes), 2)
|
||||
combinations = [tags for n in range(0, max_tags + 1) for tags in itertools.combinations(suffixes, n)]
|
||||
for tags in extra_combinations:
|
||||
if tags not in combinations:
|
||||
combinations.append(tags)
|
||||
|
||||
inline_namespaces = []
|
||||
for tags in combinations:
|
||||
ns = abi_prefix + ''.join(tags)
|
||||
inline_namespaces += [ns, ns + version]
|
||||
|
||||
namespaces += [f'{namespaces[0]}::{ns}' for ns in inline_namespaces]
|
||||
|
||||
@@ -91,5 +144,10 @@ if __name__ == '__main__':
|
||||
template = env.get_template('nlohmann_json.natvis.j2')
|
||||
natvis = template.render(namespaces=namespaces)
|
||||
|
||||
with open(os.path.join(args.output, 'nlohmann_json.natvis'), 'w') as f:
|
||||
output = args.output or os.path.join(args.repo_root, 'nlohmann_json.natvis')
|
||||
with open(output, 'w') as f:
|
||||
f.write(natvis)
|
||||
|
||||
if args.all:
|
||||
print(f'wrote {len(namespaces)} namespaces ({os.path.getsize(output) / 1e6:.1f} MB) to {output}',
|
||||
file=sys.stderr)
|
||||
@@ -0,0 +1 @@
|
||||
jinja2==3.1.6
|
||||
@@ -1 +1,163 @@
|
||||
jinja2==3.1.6
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.14
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --generate-hashes --no-emit-index-url --output-file=tools/generate_natvis/requirements.txt --strip-extras tools/generate_natvis/requirements.in
|
||||
#
|
||||
jinja2==3.1.6 \
|
||||
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
|
||||
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
|
||||
# via -r tools/generate_natvis/requirements.in
|
||||
markupsafe==3.0.4 \
|
||||
--hash=sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98 \
|
||||
--hash=sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002 \
|
||||
--hash=sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b \
|
||||
--hash=sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653 \
|
||||
--hash=sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c \
|
||||
--hash=sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e \
|
||||
--hash=sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc \
|
||||
--hash=sha256:0764a13d34cae40db7bbf3a09b7e9b491bf4603e20b263a7a9d6b8e324975d0a \
|
||||
--hash=sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92 \
|
||||
--hash=sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f \
|
||||
--hash=sha256:0cee7cb0f9a1b6892ea482237d9403b3d1b4603aee057d0ff01f0fac2d019a97 \
|
||||
--hash=sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4 \
|
||||
--hash=sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7 \
|
||||
--hash=sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691 \
|
||||
--hash=sha256:14bd2d845d62ab678eaf81da89d7b621b51756c72346745c1a594c09d49207a2 \
|
||||
--hash=sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc \
|
||||
--hash=sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde \
|
||||
--hash=sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99 \
|
||||
--hash=sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9 \
|
||||
--hash=sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df \
|
||||
--hash=sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5 \
|
||||
--hash=sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17 \
|
||||
--hash=sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8 \
|
||||
--hash=sha256:2a6ef68ae94aed8721934072b27a3b654ea2100b97e4ab864cf1489c90926fbc \
|
||||
--hash=sha256:2b2b1e18af909b448bb3cf9e3433366f7a8726271fc214e8b10e0f62a78c724b \
|
||||
--hash=sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea \
|
||||
--hash=sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248 \
|
||||
--hash=sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741 \
|
||||
--hash=sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5 \
|
||||
--hash=sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6 \
|
||||
--hash=sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7 \
|
||||
--hash=sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1 \
|
||||
--hash=sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67 \
|
||||
--hash=sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f \
|
||||
--hash=sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9 \
|
||||
--hash=sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c \
|
||||
--hash=sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc \
|
||||
--hash=sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba \
|
||||
--hash=sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17 \
|
||||
--hash=sha256:3d23795802fc8bd72534836d64489bbf0f67c088959091bdb22e10735a5107bf \
|
||||
--hash=sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6 \
|
||||
--hash=sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2 \
|
||||
--hash=sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163 \
|
||||
--hash=sha256:4a540e2d3192792fc84eced57bef37851ccb2b41f73291bb17408eea77bcd278 \
|
||||
--hash=sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d \
|
||||
--hash=sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b \
|
||||
--hash=sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634 \
|
||||
--hash=sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38 \
|
||||
--hash=sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed \
|
||||
--hash=sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c \
|
||||
--hash=sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148 \
|
||||
--hash=sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a \
|
||||
--hash=sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7 \
|
||||
--hash=sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f \
|
||||
--hash=sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811 \
|
||||
--hash=sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e \
|
||||
--hash=sha256:57f9947a7e57a081c1e3e0a2dd0d2dcf290a4531450e6f611e30084c222a7295 \
|
||||
--hash=sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2 \
|
||||
--hash=sha256:5c22873ad1f0532ba40fa1727f3c0fc1bbbaab6d373d4cbe3f0dc74b2e2521c7 \
|
||||
--hash=sha256:5e8b3d0b18fd623afa12ecb2ce8d8becef69f9b5440c6330c7972200e0bb84b0 \
|
||||
--hash=sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6 \
|
||||
--hash=sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed \
|
||||
--hash=sha256:6669c1bf34080161ce49c589cc512ef24d4c704ac9d2b2d3667f519c60418378 \
|
||||
--hash=sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0 \
|
||||
--hash=sha256:6768d67d1bce64270e0fdc2e69309d68b9b18ae56ddf6c711d168e9d051c2cac \
|
||||
--hash=sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b \
|
||||
--hash=sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96 \
|
||||
--hash=sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59 \
|
||||
--hash=sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808 \
|
||||
--hash=sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2 \
|
||||
--hash=sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb \
|
||||
--hash=sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65 \
|
||||
--hash=sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72 \
|
||||
--hash=sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8 \
|
||||
--hash=sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e \
|
||||
--hash=sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91 \
|
||||
--hash=sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a \
|
||||
--hash=sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2 \
|
||||
--hash=sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e \
|
||||
--hash=sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707 \
|
||||
--hash=sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21 \
|
||||
--hash=sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef \
|
||||
--hash=sha256:8698d70a8081ee8c090dbb394768b5789a1da8b131b5499f89d071dd3cfaf6be \
|
||||
--hash=sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453 \
|
||||
--hash=sha256:88d59b473bfb03259722600839af9bbd7fa13a2eb514beefeedb95997882f69a \
|
||||
--hash=sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6 \
|
||||
--hash=sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977 \
|
||||
--hash=sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978 \
|
||||
--hash=sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581 \
|
||||
--hash=sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692 \
|
||||
--hash=sha256:9240187afb63d2f9ddc3e032c670356fe941f6e20662ea168a5dc3f1f317e1b3 \
|
||||
--hash=sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369 \
|
||||
--hash=sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a \
|
||||
--hash=sha256:9388003072b95f2f1e3fd908604194d653ba21330d811961a78b7da1a77e9e36 \
|
||||
--hash=sha256:9438a2648b2195980cb2dd8e53ed7b8df91319e2d0b70ae61a9e1d1bc8d3bec9 \
|
||||
--hash=sha256:94e4c421742086aeee4c32a506eec8859d7634aad943f7e6aacf70f813478768 \
|
||||
--hash=sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916 \
|
||||
--hash=sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b \
|
||||
--hash=sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f \
|
||||
--hash=sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346 \
|
||||
--hash=sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c \
|
||||
--hash=sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464 \
|
||||
--hash=sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9 \
|
||||
--hash=sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee \
|
||||
--hash=sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300 \
|
||||
--hash=sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6 \
|
||||
--hash=sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d \
|
||||
--hash=sha256:ac0c7c9f1609b0c4c114feb1d7a3409564c7fb77e360bed9e97e5d25dfeaf868 \
|
||||
--hash=sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46 \
|
||||
--hash=sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97 \
|
||||
--hash=sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733 \
|
||||
--hash=sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe \
|
||||
--hash=sha256:b61687d0828e72bf5cda24a2690188f37170bd31c9359ac97e4e66569f120a16 \
|
||||
--hash=sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429 \
|
||||
--hash=sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39 \
|
||||
--hash=sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894 \
|
||||
--hash=sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c \
|
||||
--hash=sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c \
|
||||
--hash=sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169 \
|
||||
--hash=sha256:bf053da3c97a4bc5ecfbb218cdd2983febd91c617be8367d139882aa11e490aa \
|
||||
--hash=sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77 \
|
||||
--hash=sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe \
|
||||
--hash=sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad \
|
||||
--hash=sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85 \
|
||||
--hash=sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e \
|
||||
--hash=sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34 \
|
||||
--hash=sha256:cf63c214fe879a65e69a386f915e36104fc84254ab141240f8854602d8e0be2a \
|
||||
--hash=sha256:d1aca03ede943eb80ab3d63bb082c84b7aab85ea83bd0fd0c200260945fb49d9 \
|
||||
--hash=sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c \
|
||||
--hash=sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749 \
|
||||
--hash=sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214 \
|
||||
--hash=sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932 \
|
||||
--hash=sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494 \
|
||||
--hash=sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889 \
|
||||
--hash=sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1 \
|
||||
--hash=sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0 \
|
||||
--hash=sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2 \
|
||||
--hash=sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786 \
|
||||
--hash=sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78 \
|
||||
--hash=sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e \
|
||||
--hash=sha256:e841068dc0be4cb6dfb5c890eb88cbdcff2f4a332393c7ec94e8e618bd32c1a8 \
|
||||
--hash=sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289 \
|
||||
--hash=sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c \
|
||||
--hash=sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe \
|
||||
--hash=sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237 \
|
||||
--hash=sha256:f291bcf42ae98eb5107edb162c3c998b4a89648fd8e99ed4cbd12705292788cd \
|
||||
--hash=sha256:f61efe1d2fe0de16158a5fe1d1cf3c14bdb6aecd54d8938fd26512c525c1f624 \
|
||||
--hash=sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19 \
|
||||
--hash=sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977 \
|
||||
--hash=sha256:fd9f8797427910198f95bced71ddfed61130d7e349213bfb8466c9c99e2c46a8 \
|
||||
--hash=sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3
|
||||
# via jinja2
|
||||
Reference in new issue
Block a user