mirror of
https://github.com/nlohmann/json.git
synced 2026-10-22 14:08:13 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89bd5d63e1 | ||
|
|
3fb7d9c710 | ||
|
|
fa8134ae85 | ||
|
|
2dc1e54856 | ||
|
|
9855db81f3 | ||
|
|
533cbfee51 | ||
|
|
9bf531436f | ||
|
|
3e031b06a5 | ||
|
|
7c26e99ac5 | ||
|
|
6aefd8c662 | ||
|
|
52fecc1fab | ||
|
|
fa37b952df | ||
|
|
e9970066c5 | ||
|
|
3f8677d028 | ||
|
|
ef3d350639 |
No files matched your search
@@ -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)/.*"
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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@e14015d583714f6e62063499dc959a02595150a1 # v2.21.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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: json_view-benchmarks-${{ matrix.runner }}-${{ inputs.compiler || 'g++' }}
|
||||
path: tests/benchmarks/json_view/results/
|
||||
@@ -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:
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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():
|
||||
@@ -1841,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;
|
||||
@@ -1861,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
|
||||
/////////////////////
|
||||
@@ -2486,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 '\"':
|
||||
|
||||
@@ -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
|
||||
@@ -758,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)
|
||||
|
||||
@@ -244,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;
|
||||
|
||||
@@ -290,7 +291,7 @@ class parser
|
||||
}
|
||||
|
||||
// remember we are now inside an object
|
||||
states.push_back(false);
|
||||
states.push_back(0);
|
||||
|
||||
// parse values
|
||||
get_token();
|
||||
@@ -315,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;
|
||||
@@ -432,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm> // all_of
|
||||
#include <array> // array
|
||||
#include <cctype> // isdigit
|
||||
#include <cerrno> // errno, ERANGE
|
||||
#include <cstddef> // size_t
|
||||
#include <cstdint> // uint64_t
|
||||
#include <cstdlib> // strtoull
|
||||
#include <cstring> // memcmp, memcpy
|
||||
#ifndef JSON_NO_IO
|
||||
#include <iosfwd> // ostream
|
||||
#endif // JSON_NO_IO
|
||||
@@ -24,6 +27,7 @@
|
||||
#include <nlohmann/detail/exceptions.hpp>
|
||||
#include <nlohmann/detail/macro_scope.hpp>
|
||||
#include <nlohmann/detail/meta/cpp_future.hpp>
|
||||
#include <nlohmann/detail/meta/type_traits.hpp>
|
||||
#include <nlohmann/detail/string_concat.hpp>
|
||||
#include <nlohmann/detail/string_escape.hpp>
|
||||
#include <nlohmann/detail/string_utils.hpp>
|
||||
@@ -499,50 +503,111 @@ class json_pointer
|
||||
|
||||
private:
|
||||
/*!
|
||||
@brief the pointer prefixes of a flattened object, and which of them denote arrays
|
||||
@brief the pointer prefixes of a flattened object that denote arrays
|
||||
|
||||
@ref unflatten collects the pointer prefixes that have a reference token 0
|
||||
among their children; @ref get_and_create creates arrays exactly below
|
||||
those prefixes and objects everywhere else. Deciding this up front keeps
|
||||
the result independent of the order in which the flattened object is
|
||||
iterated, which is unspecified for some object types.
|
||||
@ref unflatten creates arrays exactly below the prefixes that have a
|
||||
reference token 0 among their children, and objects everywhere else.
|
||||
Deciding this up front keeps the result independent of the order in which
|
||||
the flattened object is iterated, which is unspecified for some object
|
||||
types.
|
||||
|
||||
The prefixes form a tree and are numbered (number 0 is the empty prefix),
|
||||
so each of them is stored only once (as a node) rather than as a copy of
|
||||
all of its reference tokens. The tree does not own the reference tokens:
|
||||
it refers to the tokens of the parsed keys, which must outlive it.
|
||||
A prefix is identified by the text of the key it is the beginning of:
|
||||
escaping is unambiguous, so equal prefixes of valid keys are equal texts.
|
||||
The set refers to the keys, which must outlive it.
|
||||
*/
|
||||
struct prefix_tree
|
||||
class array_prefix_set
|
||||
{
|
||||
// open-addressing hash table from (parent, last reference token) to
|
||||
// the number of the node; empty slots have token == nullptr
|
||||
public:
|
||||
void insert(const char* data, std::size_t size)
|
||||
{
|
||||
// flattened objects are usually sorted, so most keys that have a
|
||||
// 0 yield the prefix of the previous one
|
||||
if (last_data != nullptr && last_size == size && std::memcmp(last_data, data, size) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
last_data = data;
|
||||
last_size = size;
|
||||
|
||||
if (table.empty())
|
||||
{
|
||||
table.resize(16);
|
||||
}
|
||||
const std::size_t hash = hash_of(data, size);
|
||||
const std::size_t mask = table.size() - 1;
|
||||
std::size_t i = hash & mask;
|
||||
while (table[i].data != nullptr)
|
||||
{
|
||||
if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
table[i] = slot(hash, data, size);
|
||||
|
||||
// keep the load factor below 1/2
|
||||
if (++used * 2 > table.size())
|
||||
{
|
||||
grow();
|
||||
}
|
||||
}
|
||||
|
||||
bool empty() const noexcept
|
||||
{
|
||||
return used == 0;
|
||||
}
|
||||
|
||||
bool contains(const char* data, std::size_t size) const noexcept
|
||||
{
|
||||
if (used == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t hash = hash_of(data, size);
|
||||
const std::size_t mask = table.size() - 1;
|
||||
for (std::size_t i = hash & mask; table[i].data != nullptr; i = (i + 1) & mask)
|
||||
{
|
||||
if (table[i].hash == hash && table[i].size == size && std::memcmp(table[i].data, data, size) == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
struct slot
|
||||
{
|
||||
std::size_t hash;
|
||||
std::size_t parent;
|
||||
const string_t* token;
|
||||
std::size_t id;
|
||||
slot() noexcept = default;
|
||||
|
||||
slot() noexcept
|
||||
: hash(0), parent(0), token(nullptr), id(0)
|
||||
slot(std::size_t h, const char* d, std::size_t s) noexcept
|
||||
: hash(h), data(d), size(s)
|
||||
{}
|
||||
|
||||
slot(std::size_t h, std::size_t p, const string_t* t, std::size_t i) noexcept
|
||||
: hash(h), parent(p), token(t), id(i)
|
||||
{}
|
||||
std::size_t hash = 0;
|
||||
const char* data = nullptr; // nullptr: the slot is empty
|
||||
std::size_t size = 0;
|
||||
};
|
||||
|
||||
std::vector<slot> table;
|
||||
std::size_t used = 0;
|
||||
|
||||
static std::size_t hash_of(std::size_t parent, const string_t& token) noexcept
|
||||
static std::size_t hash_of(const char* data, std::size_t size) noexcept
|
||||
{
|
||||
std::size_t h = parent * static_cast<std::size_t>(0x9E3779B97F4A7C15ULL & (std::numeric_limits<std::size_t>::max)());
|
||||
for (std::size_t i = 0; i < token.size(); ++i)
|
||||
std::uint64_t h = 0x9E3779B97F4A7C15ULL ^ size;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= size; i += 8)
|
||||
{
|
||||
h = (h ^ static_cast<unsigned char>(token[i])) * static_cast<std::size_t>(0x100000001B3ULL & (std::numeric_limits<std::size_t>::max)());
|
||||
std::uint64_t w = 0;
|
||||
std::memcpy(&w, data + i, 8);
|
||||
h = (h ^ w) * 0x100000001B3ULL;
|
||||
h ^= h >> 32u;
|
||||
}
|
||||
return h ^ (h >> 29u);
|
||||
std::uint64_t w = 0;
|
||||
if (i < size)
|
||||
{
|
||||
std::memcpy(&w, data + i, size - i);
|
||||
}
|
||||
h = (h ^ w) * 0x9E3779B97F4A7C15ULL;
|
||||
return detail::conditional_static_cast<std::size_t>(h ^ (h >> 29u));
|
||||
}
|
||||
|
||||
void grow()
|
||||
@@ -552,10 +617,10 @@ class json_pointer
|
||||
const std::size_t mask = table.size() - 1;
|
||||
for (const auto& s : old)
|
||||
{
|
||||
if (s.token != nullptr)
|
||||
if (s.data != nullptr)
|
||||
{
|
||||
std::size_t i = s.hash & mask;
|
||||
while (table[i].token != nullptr)
|
||||
while (table[i].data != nullptr)
|
||||
{
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
@@ -564,117 +629,160 @@ class json_pointer
|
||||
}
|
||||
}
|
||||
|
||||
// parent[id] is the number of the prefix without its last token
|
||||
std::vector<std::size_t> parent;
|
||||
// is_array[id] is true iff some flattened key has the reference token
|
||||
// 0 directly below the prefix with number id
|
||||
std::vector<bool> is_array;
|
||||
|
||||
// start with the empty prefix only
|
||||
prefix_tree()
|
||||
: table(16)
|
||||
, parent(1, 0)
|
||||
, is_array(1, false)
|
||||
{}
|
||||
|
||||
// return the number of the prefix with number id extended by
|
||||
// reference_token, adding it if it is new
|
||||
std::size_t add_child(std::size_t id, const string_t& reference_token)
|
||||
{
|
||||
const std::size_t hash = hash_of(id, reference_token);
|
||||
const std::size_t mask = table.size() - 1;
|
||||
std::size_t i = hash & mask;
|
||||
while (table[i].token != nullptr)
|
||||
{
|
||||
if (table[i].hash == hash && table[i].parent == id && *table[i].token == reference_token)
|
||||
{
|
||||
return table[i].id;
|
||||
}
|
||||
i = (i + 1) & mask;
|
||||
}
|
||||
|
||||
// new prefix
|
||||
const std::size_t child = parent.size();
|
||||
table[i] = slot(hash, id, &reference_token, child);
|
||||
if (reference_token == "0")
|
||||
{
|
||||
is_array[id] = true;
|
||||
}
|
||||
parent.push_back(id);
|
||||
is_array.push_back(false);
|
||||
|
||||
// keep the load factor below 1/2
|
||||
if (++used * 2 > table.size())
|
||||
{
|
||||
grow();
|
||||
}
|
||||
return child;
|
||||
}
|
||||
std::vector<slot> table{};
|
||||
std::size_t used = 0;
|
||||
const char* last_data = nullptr;
|
||||
std::size_t last_size = 0;
|
||||
};
|
||||
|
||||
/// @brief the length of the longest common prefix of the first @a limit bytes of @a a and @a b
|
||||
static std::size_t common_prefix_length(const char* a, const char* b, const std::size_t limit) noexcept
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= limit; i += 8)
|
||||
{
|
||||
std::uint64_t x = 0;
|
||||
std::uint64_t y = 0;
|
||||
std::memcpy(&x, a + i, 8);
|
||||
std::memcpy(&y, b + i, 8);
|
||||
if (x != y)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (i < limit && a[i] == b[i])
|
||||
{
|
||||
++i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief create and return a reference to the pointed to value
|
||||
@brief check that @a key is a valid JSON pointer, and add the prefixes of
|
||||
it that denote arrays to @a arrays
|
||||
|
||||
@param[in] j the value to create the pointed to value in
|
||||
@param[in] tokens the reference tokens of the pointer
|
||||
@param[in] prefixes prefixes[i] is the number in @a tree of the prefix
|
||||
consisting of the first i reference tokens
|
||||
@throw parse_error.107 if the key is not empty or begins with '/'
|
||||
@throw parse_error.108 if character '~' is not followed by '0' or '1'
|
||||
|
||||
Complexity: Linear in the number of reference tokens.
|
||||
The same checks as @ref split, which throws the same exceptions, but
|
||||
without building the reference tokens.
|
||||
*/
|
||||
static void scan_key(const string_t& key, const string_t* previous, array_prefix_set& arrays)
|
||||
{
|
||||
const std::size_t size = key.size();
|
||||
if (size == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@throw parse_error.106 if an array index begins with '0'
|
||||
@throw parse_error.109 if array index is not a number
|
||||
@throw type_error.313 if value cannot be unflattened
|
||||
const char* const data = key.data();
|
||||
if (JSON_HEDLEY_UNLIKELY(data[0] != '/'))
|
||||
{
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_missing_slash, 1, detail::concat("JSON pointer must be empty or begin with '/' - was: '", key, "'"), nullptr));
|
||||
}
|
||||
|
||||
// The beginning of the key that it has in common with the previous
|
||||
// one, which was checked already, is skipped: the scan starts with
|
||||
// the token after the last slash in it.
|
||||
std::size_t begin = 1;
|
||||
if (previous != nullptr)
|
||||
{
|
||||
std::size_t same = common_prefix_length(data, previous->data(), (std::min)(size, previous->size()));
|
||||
while (same > 1 && data[same - 1] != '/')
|
||||
{
|
||||
--same;
|
||||
}
|
||||
if (same > 1)
|
||||
{
|
||||
begin = same;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = begin; i <= size; ++i)
|
||||
{
|
||||
if (i == size || data[i] == '/')
|
||||
{
|
||||
// a token 0 makes the pointer before it an array
|
||||
if (i - begin == 1 && data[begin] == '0')
|
||||
{
|
||||
arrays.insert(data, begin - 1);
|
||||
}
|
||||
begin = i + 1;
|
||||
}
|
||||
else if (data[i] == '~')
|
||||
{
|
||||
// ~ must be followed by 0 or 1
|
||||
if (JSON_HEDLEY_UNLIKELY(i + 1 == size || (data[i + 1] != '0' && data[i + 1] != '1')))
|
||||
{
|
||||
JSON_THROW(detail::parse_error::create(detail::exception_id::pointer_invalid_escape, 0, "escape character '~' must be followed with '0' or '1'", nullptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief resolve a plain JSON pointer without building its reference tokens
|
||||
|
||||
@param[in] ptr the value the pointer starts at
|
||||
@param[in] path a JSON pointer string
|
||||
@param[in] length the number of leading bytes of @a path to resolve; 0 or
|
||||
the position of a slash
|
||||
@param[in,out] token a string to hold the reference tokens, to reuse its storage
|
||||
|
||||
@return the pointed to value, or `nullptr` if the pointer does not denote
|
||||
an existing value, or does so in a way that is not plain: a
|
||||
reference token with an escape, an array index that is malformed or
|
||||
out of range, or a "-". No exception is thrown for these; callers
|
||||
use @ref json_pointer instead, which handles them.
|
||||
*/
|
||||
template<typename BasicJsonType>
|
||||
static BasicJsonType& get_and_create(BasicJsonType& j, const std::vector<string_t>& tokens,
|
||||
const std::vector<std::size_t>& prefixes, const prefix_tree& tree)
|
||||
static BasicJsonType* resolve_plain(BasicJsonType* ptr, const string_t& path, const std::size_t length, string_t& token)
|
||||
{
|
||||
auto* result = &j;
|
||||
|
||||
// in case no reference tokens exist, return a reference to the JSON value
|
||||
// j which will be overwritten by a primitive value
|
||||
for (std::size_t i = 0; i < tokens.size(); ++i)
|
||||
for (std::size_t pos = 0; pos < length;)
|
||||
{
|
||||
const auto& reference_token = tokens[i];
|
||||
switch (result->type())
|
||||
if (JSON_HEDLEY_UNLIKELY(path[pos] != '/'))
|
||||
{
|
||||
case detail::value_t::null:
|
||||
{
|
||||
if (tree.is_array[prefixes[i]])
|
||||
{
|
||||
// some reference token below this position is 0, so the
|
||||
// value is an array
|
||||
result = &result->operator[](array_index<BasicJsonType>(reference_token));
|
||||
}
|
||||
else
|
||||
{
|
||||
// start a new object otherwise
|
||||
result = &result->operator[](reference_token);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// the reference token between the slash at pos and the next one
|
||||
std::size_t end = path.find_first_of('/', pos + 1);
|
||||
if (end == string_t::npos || end > length)
|
||||
{
|
||||
end = length;
|
||||
}
|
||||
token.clear();
|
||||
token.append(path.data() + pos + 1, end - pos - 1);
|
||||
if (JSON_HEDLEY_UNLIKELY(token.find_first_of('~') != string_t::npos))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
switch (ptr->type())
|
||||
{
|
||||
case detail::value_t::object:
|
||||
{
|
||||
// create an entry in the object
|
||||
result = &object_member(*result, reference_token);
|
||||
const auto it = ptr->m_data.m_value.object->find(token);
|
||||
if (JSON_HEDLEY_UNLIKELY(it == ptr->m_data.m_value.object->end()))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ptr = &it->second;
|
||||
break;
|
||||
}
|
||||
|
||||
case detail::value_t::array:
|
||||
{
|
||||
// create an entry in the array
|
||||
result = &result->operator[](array_index<BasicJsonType>(reference_token));
|
||||
typename BasicJsonType::size_type idx{};
|
||||
if (JSON_HEDLEY_UNLIKELY(parse_array_index<BasicJsonType>(token, idx) != array_index_status::ok || idx >= ptr->m_data.m_value.array->size()))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
ptr = &(*ptr->m_data.m_value.array)[idx];
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
The following code is only reached if there exists a reference
|
||||
token _and_ the current value is primitive. In this case, we have
|
||||
an error situation, because primitive values may only occur as
|
||||
a single value; that is, with an empty list of reference tokens.
|
||||
*/
|
||||
case detail::value_t::null:
|
||||
case detail::value_t::string:
|
||||
case detail::value_t::boolean:
|
||||
case detail::value_t::number_integer:
|
||||
@@ -683,11 +791,12 @@ class json_pointer
|
||||
case detail::value_t::binary:
|
||||
case detail::value_t::discarded:
|
||||
default:
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &j));
|
||||
return nullptr;
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
|
||||
return *result;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -1142,20 +1251,45 @@ class json_pointer
|
||||
// Add the entry (key, v) to the object `obj`. Flattened paths are pairwise
|
||||
// distinct, so objects that can append a key known to be absent
|
||||
// (ordered_map) skip the lookup, which is linear and would make flatten
|
||||
// quadratic; all other object types use operator[].
|
||||
// quadratic. Objects with a positional insert (std::map) insert next to
|
||||
// the entry added last: paths come out in nearly sorted order, so the
|
||||
// position is usually right and no search from the root is needed.
|
||||
// All other object types use operator[].
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static auto add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/)
|
||||
static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<2> /*unused*/)
|
||||
-> decltype(obj.emplace_new_key(key, std::forward<ValueType>(v)), void())
|
||||
{
|
||||
obj.emplace_new_key(key, std::forward<ValueType>(v));
|
||||
}
|
||||
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static void add_flattened(ObjectType& obj, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/)
|
||||
static auto add_flattened(ObjectType& obj, typename ObjectType::iterator& hint, const string_t& key, ValueType&& v, detail::priority_tag<1> /*unused*/)
|
||||
-> decltype(hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v)), void())
|
||||
{
|
||||
hint = obj.emplace_hint(hint, key, std::forward<ValueType>(v));
|
||||
++hint;
|
||||
}
|
||||
|
||||
template<typename ObjectType, typename ValueType>
|
||||
static void add_flattened(ObjectType& obj, typename ObjectType::iterator& /*hint*/, const string_t& key, ValueType&& v, detail::priority_tag<0> /*unused*/)
|
||||
{
|
||||
obj[key] = std::forward<ValueType>(v);
|
||||
}
|
||||
|
||||
/// append the decimal representation of @a i to @a out
|
||||
static void append_index(string_t& out, std::size_t i)
|
||||
{
|
||||
std::array<char, 24> buf{};
|
||||
std::size_t pos = buf.size();
|
||||
do
|
||||
{
|
||||
buf[--pos] = static_cast<char>('0' + (i % 10));
|
||||
i /= 10;
|
||||
}
|
||||
while (i != 0);
|
||||
out.append(buf.data() + pos, buf.size() - pos);
|
||||
}
|
||||
|
||||
/*!
|
||||
@param[in] reference_string the reference string to the current value
|
||||
@param[in] value the value to consider
|
||||
@@ -1201,7 +1335,8 @@ class json_pointer
|
||||
// flatten `v`, whose path is `path`: primitives and empty containers
|
||||
// are added to the result right away; other containers get a frame
|
||||
auto& object = *result.m_data.m_value.object;
|
||||
const auto enter = [&stack, &path, &object](const BasicJsonType & v)
|
||||
auto hint = object.end();
|
||||
const auto enter = [&stack, &path, &object, &hint](const BasicJsonType & v)
|
||||
{
|
||||
switch (v.type())
|
||||
{
|
||||
@@ -1210,7 +1345,7 @@ class json_pointer
|
||||
if (v.m_data.m_value.array->empty())
|
||||
{
|
||||
// flatten empty array as null
|
||||
add_flattened(object, path, nullptr, detail::priority_tag<1> {});
|
||||
add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1224,7 +1359,7 @@ class json_pointer
|
||||
if (v.m_data.m_value.object->empty())
|
||||
{
|
||||
// flatten empty object as null
|
||||
add_flattened(object, path, nullptr, detail::priority_tag<1> {});
|
||||
add_flattened(object, hint, path, nullptr, detail::priority_tag<2> {});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1244,7 +1379,7 @@ class json_pointer
|
||||
default:
|
||||
{
|
||||
// add a primitive value with its reference string
|
||||
add_flattened(object, path, v, detail::priority_tag<1> {});
|
||||
add_flattened(object, hint, path, v, detail::priority_tag<2> {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1272,7 +1407,8 @@ class json_pointer
|
||||
|
||||
// iterate array and use index as a reference string
|
||||
++stack.back().index;
|
||||
detail::concat_into(path, '/', detail::to_string<string_t>(i));
|
||||
path.push_back('/');
|
||||
append_index(path, i);
|
||||
enter(array[i]);
|
||||
}
|
||||
else
|
||||
@@ -1286,7 +1422,8 @@ class json_pointer
|
||||
|
||||
// iterate object and use keys as reference string
|
||||
++stack.back().member;
|
||||
detail::concat_into(path, '/', detail::escape(it->first));
|
||||
path.push_back('/');
|
||||
detail::escape_into(path, it->first);
|
||||
enter(it->second);
|
||||
}
|
||||
}
|
||||
@@ -1317,53 +1454,30 @@ class json_pointer
|
||||
|
||||
BasicJsonType result;
|
||||
|
||||
// parse every key once; the reference tokens are kept for the second
|
||||
// pass (the vectors are never resized, so the tree may refer to them)
|
||||
std::vector<std::vector<string_t>> keys;
|
||||
keys.reserve(value.m_data.m_value.object->size());
|
||||
// check every key, and collect the prefixes that have a reference
|
||||
// token 0 among their children; the values below them are arrays,
|
||||
// all others are objects (see array_prefix_set)
|
||||
array_prefix_set arrays;
|
||||
const string_t* previous_key = nullptr;
|
||||
for (const auto& element : *value.m_data.m_value.object)
|
||||
{
|
||||
json_pointer ptr(element.first);
|
||||
keys.push_back(std::move(ptr.reference_tokens));
|
||||
scan_key(element.first, previous_key, arrays);
|
||||
previous_key = &element.first;
|
||||
}
|
||||
|
||||
// collect the pointer prefixes that have a reference token 0 among
|
||||
// their children; the values below them are arrays, all others are
|
||||
// objects (see prefix_tree)
|
||||
prefix_tree tree;
|
||||
// the number of the prefix with i + 1 tokens of the previous key; keys
|
||||
// are often sorted, so most of the prefix is the same as for the
|
||||
// previous key and need not be looked up again
|
||||
std::vector<std::size_t> path;
|
||||
const std::vector<string_t>* previous = nullptr;
|
||||
// the number of the complete prefix (i.e., the key) of each key
|
||||
std::vector<std::size_t> leaves;
|
||||
leaves.reserve(keys.size());
|
||||
for (const auto& tokens : keys)
|
||||
{
|
||||
std::size_t common = 0;
|
||||
if (previous != nullptr)
|
||||
{
|
||||
const std::size_t limit = (std::min)(tokens.size(), previous->size());
|
||||
while (common < limit && tokens[common] == (*previous)[common])
|
||||
{
|
||||
++common;
|
||||
}
|
||||
}
|
||||
path.resize(common);
|
||||
std::size_t id = common == 0 ? 0 : path[common - 1];
|
||||
for (std::size_t i = common; i < tokens.size(); ++i)
|
||||
{
|
||||
id = tree.add_child(id, tokens[i]);
|
||||
path.push_back(id);
|
||||
}
|
||||
leaves.push_back(id);
|
||||
previous = &tokens;
|
||||
}
|
||||
// values[i] is the value after the first i reference tokens of the
|
||||
// previous key, ends[i] the length of that prefix of the key. Keys are
|
||||
// often sorted, so most of the path to the value of a key is the
|
||||
// same as for the previous key and need not be looked up again. Only
|
||||
// the values below the common prefix are dropped, and only they can
|
||||
// move when a value is added: the ones above are not inside the
|
||||
// container that grows.
|
||||
std::vector<BasicJsonType*> values(1, &result);
|
||||
std::vector<std::size_t> ends(1, 0);
|
||||
const string_t* previous = nullptr;
|
||||
string_t token;
|
||||
|
||||
// iterate the JSON object values
|
||||
std::vector<std::size_t> prefixes;
|
||||
std::size_t key_index = 0;
|
||||
for (const auto& element : *value.m_data.m_value.object)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive()))
|
||||
@@ -1371,21 +1485,100 @@ class json_pointer
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_value_not_primitive, "values in object must be primitive", &element.second));
|
||||
}
|
||||
|
||||
const auto& tokens = keys[key_index];
|
||||
// number the prefixes of this key from its last one to the first
|
||||
prefixes.resize(tokens.size());
|
||||
for (std::size_t i = tokens.size(), id = leaves[key_index]; i > 0; --i)
|
||||
const string_t& key = element.first;
|
||||
const char* const data = key.data();
|
||||
|
||||
// the number of reference tokens the key has in common with the
|
||||
// previous one
|
||||
std::size_t depth = 0;
|
||||
if (previous != nullptr)
|
||||
{
|
||||
id = tree.parent[id];
|
||||
prefixes[i - 1] = id;
|
||||
const std::size_t same = common_prefix_length(data, previous->data(), (std::min)(key.size(), previous->size()));
|
||||
depth = ends.size() - 1;
|
||||
while (depth > 0 && (ends[depth] > same || (ends[depth] == same && same < key.size() && data[same] != '/')))
|
||||
{
|
||||
--depth;
|
||||
}
|
||||
}
|
||||
++key_index;
|
||||
values.resize(depth + 1);
|
||||
ends.resize(depth + 1);
|
||||
previous = &key;
|
||||
|
||||
// Assign the value to the reference pointed to by JSON pointer. Note
|
||||
// that if the JSON pointer is "" (i.e., points to the whole value),
|
||||
// function get_and_create returns a reference to the result itself.
|
||||
// An assignment will then create a primitive value.
|
||||
get_and_create(result, tokens, prefixes, tree) = element.second;
|
||||
// the value is the result itself. An assignment will then create a
|
||||
// primitive value.
|
||||
BasicJsonType* current = values.back();
|
||||
for (std::size_t pos = ends.back(); pos < key.size();)
|
||||
{
|
||||
// the reference token between the slash at pos and the next
|
||||
std::size_t end = key.find_first_of('/', pos + 1);
|
||||
if (end == string_t::npos)
|
||||
{
|
||||
end = key.size();
|
||||
}
|
||||
token.clear();
|
||||
token.append(data + pos + 1, end - pos - 1);
|
||||
if (token.find_first_of('~') != string_t::npos)
|
||||
{
|
||||
detail::unescape(token);
|
||||
}
|
||||
|
||||
switch (current->type())
|
||||
{
|
||||
case detail::value_t::null:
|
||||
{
|
||||
if (arrays.contains(data, pos))
|
||||
{
|
||||
// some reference token below this position is 0, so the
|
||||
// value is an array
|
||||
current = ¤t->operator[](array_index<BasicJsonType>(token));
|
||||
}
|
||||
else
|
||||
{
|
||||
// start a new object otherwise
|
||||
current = ¤t->operator[](token);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case detail::value_t::object:
|
||||
{
|
||||
// create an entry in the object
|
||||
current = &object_member(*current, token);
|
||||
break;
|
||||
}
|
||||
|
||||
case detail::value_t::array:
|
||||
{
|
||||
// create an entry in the array
|
||||
current = ¤t->operator[](array_index<BasicJsonType>(token));
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
The following code is only reached if there exists a reference
|
||||
token _and_ the current value is primitive. In this case, we have
|
||||
an error situation, because primitive values may only occur as
|
||||
a single value; that is, with an empty list of reference tokens.
|
||||
*/
|
||||
case detail::value_t::string:
|
||||
case detail::value_t::boolean:
|
||||
case detail::value_t::number_integer:
|
||||
case detail::value_t::number_unsigned:
|
||||
case detail::value_t::number_float:
|
||||
case detail::value_t::binary:
|
||||
case detail::value_t::discarded:
|
||||
default:
|
||||
JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result));
|
||||
}
|
||||
|
||||
values.push_back(current);
|
||||
ends.push_back(end);
|
||||
pos = end;
|
||||
}
|
||||
|
||||
*current = element.second;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
#include <utility> // move
|
||||
#include <vector> // vector
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
||||
#endif
|
||||
|
||||
#include <nlohmann/detail/bit_ops.hpp>
|
||||
#include <nlohmann/detail/input/binary_reader.hpp>
|
||||
#include <nlohmann/detail/input/string_scan.hpp>
|
||||
@@ -2073,7 +2069,7 @@ class binary_writer
|
||||
{
|
||||
out[0] = to_char_type(first_byte);
|
||||
const unsigned unused_bits = 64u - ((8u << size_class) >> 1u);
|
||||
const std::uint64_t tail = is_little_endian ? byte_swap(value) >> unused_bits : value << unused_bits;
|
||||
const std::uint64_t tail = is_little_endian ? byteswap(value) >> unused_bits : value << unused_bits;
|
||||
std::memcpy(out + 1, &tail, sizeof(tail));
|
||||
return ((std::size_t{1} << size_class) >> 1u) + 1;
|
||||
}
|
||||
@@ -3517,53 +3513,14 @@ class binary_writer
|
||||
// Utility functions //
|
||||
///////////////////////
|
||||
|
||||
// single-instruction byte swaps (compilers lower these to bswap/rev/movbe);
|
||||
// used to emit big-endian numbers without a per-byte std::reverse loop
|
||||
static std::uint16_t byte_swap(std::uint16_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap16(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ushort(x);
|
||||
#else
|
||||
return static_cast<std::uint16_t>((x >> 8) | (x << 8));
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::uint32_t byte_swap(std::uint32_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap32(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_ulong(x);
|
||||
#else
|
||||
return ((x & 0x000000FFu) << 24) | ((x & 0x0000FF00u) << 8)
|
||||
| ((x & 0x00FF0000u) >> 8) | ((x & 0xFF000000u) >> 24);
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::uint64_t byte_swap(std::uint64_t x) noexcept
|
||||
{
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_bswap64(x);
|
||||
#elif defined(_MSC_VER)
|
||||
return _byteswap_uint64(x);
|
||||
#else
|
||||
x = ((x & 0x00000000FFFFFFFFull) << 32) | ((x & 0xFFFFFFFF00000000ull) >> 32);
|
||||
x = ((x & 0x0000FFFF0000FFFFull) << 16) | ((x & 0xFFFF0000FFFF0000ull) >> 16);
|
||||
x = ((x & 0x00FF00FF00FF00FFull) << 8) | ((x & 0xFF00FF00FF00FF00ull) >> 8);
|
||||
return x;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief reverse the bytes of a buffer by byte-swapping it as UIntType
|
||||
|
||||
Loading the buffer into an unsigned integer of the same width and swapping
|
||||
that is what lets the compiler emit a single bswap/rev/movbe; reversing the
|
||||
buffer element by element does not reliably get there (clang keeps a scalar
|
||||
shuffle). The two memcpy calls are the only portable way to reinterpret the
|
||||
bytes and are folded away by every optimizer.
|
||||
shuffle). (The two memcpy calls, rather than bit_cast, keep GCC from
|
||||
inlining differently in C++20, which made writing slower.)
|
||||
*/
|
||||
template<typename UIntType, std::size_t N>
|
||||
static void byte_swap_buffer(std::array<CharType, N>& a) noexcept
|
||||
@@ -3571,11 +3528,11 @@ class binary_writer
|
||||
static_assert(sizeof(UIntType) == N, "swap width must match the buffer size");
|
||||
UIntType v{};
|
||||
std::memcpy(&v, a.data(), sizeof(v));
|
||||
v = byte_swap(v);
|
||||
v = byteswap(v);
|
||||
std::memcpy(a.data(), &v, sizeof(v));
|
||||
}
|
||||
|
||||
// reverse the bytes of a fixed-size buffer; a single byte_swap() for the
|
||||
// reverse the bytes of a fixed-size buffer; a single byteswap() for the
|
||||
// common 2/4/8-byte number payloads, std::reverse for any other size
|
||||
static void reverse_bytes(std::array<CharType, 2>& a) noexcept
|
||||
{
|
||||
@@ -3700,9 +3657,7 @@ class binary_writer
|
||||
#endif
|
||||
|
||||
static_assert(sizeof(std::uint8_t) == sizeof(CharType), "size of CharType must be equal to std::uint8_t");
|
||||
CharType result;
|
||||
std::memcpy(&result, &x, sizeof(x));
|
||||
return result;
|
||||
return bit_cast<CharType>(x);
|
||||
}
|
||||
|
||||
template<typename C = CharType,
|
||||
|
||||
@@ -693,6 +693,33 @@ class serializer
|
||||
*/
|
||||
void dump_escaped(const string_t& s)
|
||||
{
|
||||
// Short strings of printable ASCII - most object keys and many values -
|
||||
// are checked byte by byte against a table and then copied in one go.
|
||||
// For them, this is cheaper than starting the vector scanner below,
|
||||
// whose setup costs more than such a string takes to scan. The same
|
||||
// check serves both values of ensure_ascii.
|
||||
if (s.size() <= 32)
|
||||
{
|
||||
const auto& copyable = ascii_copyable();
|
||||
const auto* const data = reinterpret_cast<const unsigned char*>(s.data());
|
||||
const std::size_t n = s.size();
|
||||
std::size_t k = 0;
|
||||
while (k + 4 <= n && copyable[data[k]] != 0 && copyable[data[k + 1]] != 0
|
||||
&& copyable[data[k + 2]] != 0 && copyable[data[k + 3]] != 0)
|
||||
{
|
||||
k += 4;
|
||||
}
|
||||
while (k < n && copyable[data[k]] != 0)
|
||||
{
|
||||
++k;
|
||||
}
|
||||
if (k == n)
|
||||
{
|
||||
put_string(s, 0, n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// dispatch once here rather than test the flag inside the loop: it does
|
||||
// not change while a string is written, and folding it lets each of the
|
||||
// two scanners be inlined into a loop of its own
|
||||
@@ -1073,8 +1100,12 @@ class serializer
|
||||
the indentation character once and flushing that same content repeatedly:
|
||||
flushing does not disturb what the buffer holds, so re-filling it between
|
||||
flushes would be redundant work.
|
||||
|
||||
@note inlined on purpose, as is @ref put_literal: GCC otherwise stops
|
||||
inlining both into the pretty-printing loops once @ref dump_escaped
|
||||
has grown, which made indented output up to 13% slower
|
||||
*/
|
||||
void put_indent(std::size_t indent)
|
||||
JSON_HEDLEY_ALWAYS_INLINE void put_indent(std::size_t indent)
|
||||
{
|
||||
// closing braces at the outermost level ask for no indentation at all
|
||||
if (indent == 0)
|
||||
@@ -1124,7 +1155,7 @@ class serializer
|
||||
write-through path for oversized runs.
|
||||
*/
|
||||
template<std::size_t N>
|
||||
void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
JSON_HEDLEY_ALWAYS_INLINE void put_literal(const char (&s)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
{
|
||||
static_assert(N >= 2, "put_literal expects a non-empty string literal");
|
||||
// the array bound counts the terminating NUL, which is not written
|
||||
@@ -1233,6 +1264,39 @@ class serializer
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief whether each byte can be written verbatim under either value of ensure_ascii
|
||||
|
||||
Entry c is 1 for printable ASCII other than '"' and '\\' (see
|
||||
is_ascii_copyable()) and 0 otherwise. A table lookup per byte is the
|
||||
cheapest test for the short strings handled first in dump_escaped().
|
||||
*/
|
||||
static const std::array<std::uint8_t, 256>& ascii_copyable() noexcept
|
||||
{
|
||||
static const std::array<std::uint8_t, 256> copyable =
|
||||
{
|
||||
{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x00
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x10
|
||||
1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x20
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x30
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x40
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 0x50
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 0x70
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x80
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0x90
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xA0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xB0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xC0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xD0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xE0
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 0xF0
|
||||
}
|
||||
};
|
||||
return copyable;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the JSON form of each character below 0x60
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -255,6 +255,82 @@ struct number_token
|
||||
bool is_float; ///< a fraction or an exponent
|
||||
};
|
||||
|
||||
/// advance over ASCII digits, eight bytes at a time
|
||||
inline const unsigned char* digit_run(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
while (e - p >= 8)
|
||||
{
|
||||
// (the digits become 0..9; a byte above 9, or with bit 7 set, is no digit. A carry
|
||||
// out of a byte with bit 7 set only affects the bytes above, after the first one
|
||||
// that count_trailing_zeros finds.)
|
||||
const std::uint64_t x = read_eight_bytes(p) ^ (ones * 0x30u);
|
||||
const std::uint64_t not_digit = ((x + (ones * 0x76u)) | x) & (ones * 0x80u);
|
||||
if (not_digit != 0)
|
||||
{
|
||||
return p + (static_cast<std::size_t>(count_trailing_zeros(not_digit)) / 8);
|
||||
}
|
||||
p += 8;
|
||||
}
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/// whether the n bytes at p are ASCII digits (n > 0)
|
||||
inline bool all_digits(const unsigned char* p, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
const auto digits = [](const unsigned char* q)
|
||||
{
|
||||
const std::uint64_t x = read_eight_bytes(q) ^ (ones * 0x30u);
|
||||
return (((x + (ones * 0x76u)) | x) & (ones * 0x80u)) == 0;
|
||||
};
|
||||
if (n >= 8)
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 < n; i += 8)
|
||||
{
|
||||
if (!digits(p + i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return digits(p + n - 8); // (overlapping the bytes before: they are digits)
|
||||
}
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (!is_digit(p[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Whether a float token has the shape that the parser records for most of
|
||||
/// them: [-]digits.digits, with the numbers of digits of the node's layout
|
||||
/// (extra). Then the token is a number of that layout, and the general scan
|
||||
/// that follows is not needed.
|
||||
inline bool plain_float_token(const unsigned char* s, std::size_t len, std::uint16_t extra) noexcept
|
||||
{
|
||||
const std::size_t int_digits = extra & 0xFFu;
|
||||
const std::size_t frac_digits = extra >> 8u;
|
||||
if (int_digits == 0 || int_digits >= 255 || frac_digits == 0 || frac_digits >= 255 || len == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::size_t negative = *s == '-' ? 1 : 0;
|
||||
if (len != negative + int_digits + 1 + frac_digits)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const unsigned char* const p = s + negative;
|
||||
return !(int_digits > 1 && *p == '0') && p[int_digits] == '.' && all_digits(p, int_digits) && all_digits(p + int_digits + 1, frac_digits);
|
||||
}
|
||||
|
||||
/// whether [s, s + len) is a JSON number (the grammar the parser accepts)
|
||||
inline bool scan_number_token(const unsigned char* s, std::size_t len, number_token& t)
|
||||
{
|
||||
@@ -273,11 +349,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
||||
}
|
||||
else if (*p >= '1' && *p <= '9')
|
||||
{
|
||||
do
|
||||
{
|
||||
++p;
|
||||
}
|
||||
while (p != e && is_digit(*p));
|
||||
p = digit_run(p + 1, e);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -289,10 +361,7 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
|
||||
if (p != e && *p == '.')
|
||||
{
|
||||
const unsigned char* const f0 = ++p;
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
p = digit_run(p, e);
|
||||
if (p == f0)
|
||||
{
|
||||
return false;
|
||||
@@ -403,10 +472,15 @@ struct float_range
|
||||
template<typename Check>
|
||||
bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](const byte_range & a, const byte_range & b)
|
||||
// (save() writes the ranges in the order of the nodes, which is sorted already)
|
||||
const auto before = [](const byte_range & a, const byte_range & b)
|
||||
{
|
||||
return a.off != b.off ? a.off < b.off : a.len < b.len;
|
||||
});
|
||||
};
|
||||
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), before);
|
||||
}
|
||||
std::size_t end = 0;
|
||||
for (std::size_t i = 0; i < ranges.size(); ++i)
|
||||
{
|
||||
@@ -428,7 +502,8 @@ bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
|
||||
/// record layouts that match it), tokens must not overlap.
|
||||
inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned char* text)
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b)
|
||||
// (save() writes the tokens in the order of the nodes, which is sorted already)
|
||||
const auto before = [](const float_range & a, const float_range & b)
|
||||
{
|
||||
if (a.off != b.off)
|
||||
{
|
||||
@@ -439,7 +514,11 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
||||
return a.len < b.len;
|
||||
}
|
||||
return a.extra < b.extra;
|
||||
});
|
||||
};
|
||||
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
|
||||
{
|
||||
std::sort(ranges.begin(), ranges.end(), before);
|
||||
}
|
||||
std::size_t end = 0;
|
||||
std::size_t i = 0;
|
||||
while (i < ranges.size())
|
||||
@@ -449,14 +528,18 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
||||
{
|
||||
return false;
|
||||
}
|
||||
number_token t{};
|
||||
const unsigned char* const s = text + r.off;
|
||||
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
||||
std::uint16_t layout = r.extra;
|
||||
if (!plain_float_token(s, r.len, r.extra))
|
||||
{
|
||||
return false;
|
||||
number_token t{};
|
||||
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// the digit layout the parser records (or "many", as compaction writes it)
|
||||
layout = float_layout(t);
|
||||
}
|
||||
// the digit layout the parser records (or "many", as compaction writes it)
|
||||
const std::uint16_t layout = float_layout(t);
|
||||
for (; i < ranges.size() && ranges[i].off == r.off && ranges[i].len == r.len; ++i)
|
||||
{
|
||||
if (ranges[i].extra != layout && ranges[i].extra != 0xFFFFu)
|
||||
|
||||
@@ -120,6 +120,74 @@ NLOHMANN_VIEW_ALWAYS_INLINE std::uint32_t number_length(const node& n) noexcept
|
||||
: (n.extra & 0xFFu) + (n.kind == static_cast<std::uint8_t>(value_t::number_integer) ? 1u : 0u);
|
||||
}
|
||||
|
||||
/// whether n bytes are equal; the short texts of JSON (keys, numbers, most
|
||||
/// strings) are compared by two overlapping loads instead of a library call
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE bool bytes_equal(const char* x, const char* y, std::size_t n) noexcept
|
||||
{
|
||||
const auto load8 = [](const char* p)
|
||||
{
|
||||
std::uint64_t v = 0;
|
||||
std::memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
};
|
||||
const auto load4 = [](const char* p)
|
||||
{
|
||||
std::uint32_t v = 0;
|
||||
std::memcpy(&v, p, sizeof(v));
|
||||
return v;
|
||||
};
|
||||
if (n >= 8)
|
||||
{
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 < n; i += 8)
|
||||
{
|
||||
if (load8(x + i) != load8(y + i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return load8(x + n - 8) == load8(y + n - 8);
|
||||
}
|
||||
if (n >= 4)
|
||||
{
|
||||
return load4(x) == load4(y) && load4(x + n - 4) == load4(y + n - 4);
|
||||
}
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (x[i] != y[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// whether the n1 bytes at x are before the n2 bytes at y (memcmp() order, then the shorter first);
|
||||
/// the first difference is found eight bytes at a time
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE bool bytes_less(const char* x, std::size_t n1, const char* y, std::size_t n2) noexcept
|
||||
{
|
||||
const std::size_t n = n1 < n2 ? n1 : n2;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t u = read_eight_bytes(x + i);
|
||||
const std::uint64_t v = read_eight_bytes(y + i);
|
||||
if (u != v)
|
||||
{
|
||||
const auto shift = static_cast<unsigned>(count_trailing_zeros(u ^ v)) & ~7u; // (the first byte that differs)
|
||||
return ((u >> shift) & 0xFFu) < ((v >> shift) & 0xFFu);
|
||||
}
|
||||
}
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (x[i] != y[i])
|
||||
{
|
||||
return static_cast<unsigned char>(x[i]) < static_cast<unsigned char>(y[i]);
|
||||
}
|
||||
}
|
||||
return n1 < n2;
|
||||
}
|
||||
|
||||
/// estimated number of nodes for an input of `size` bytes (one node per ~12
|
||||
/// bytes covers typical documents without regrowth)
|
||||
inline std::size_t estimate_nodes(std::size_t size) noexcept
|
||||
|
||||
@@ -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")))
|
||||
|
||||
+241
-30
@@ -7045,6 +7045,125 @@ 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;
|
||||
const string_t value_key = "value";
|
||||
const auto try_plain = [&result, &token, &value_key](patch_operations operation, const string_t& path, const basic_json & val)
|
||||
{
|
||||
if (path.empty() || path.find_first_of('~') != string_t::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// a missing "value" is reported below
|
||||
const auto value_it = val.m_data.m_value.object->find(value_key);
|
||||
const basic_json& value = value_it != val.m_data.m_value.object->end() ? value_it->second : val;
|
||||
if (operation != patch_operations::remove && value_it == val.m_data.m_value.object->end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case patch_operations::replace:
|
||||
{
|
||||
basic_json* const target = json_pointer::resolve_plain(&result, path, path.size(), token);
|
||||
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);
|
||||
// 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);
|
||||
if (parent == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
token.clear();
|
||||
token.append(path.data() + slash + 1, path.size() - slash - 1);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// iterate and apply the operations
|
||||
for (const auto& val : json_patch)
|
||||
{
|
||||
@@ -7081,9 +7200,19 @@ public:
|
||||
// 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 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, val))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
json_pointer ptr(path);
|
||||
|
||||
switch (get_op(op))
|
||||
switch (operation)
|
||||
{
|
||||
case patch_operations::add:
|
||||
{
|
||||
@@ -7191,7 +7320,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 +7365,69 @@ 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)
|
||||
{
|
||||
basic_json entry(value_t::object);
|
||||
entry["op"] = op;
|
||||
entry["path"] = path;
|
||||
if (value != nullptr)
|
||||
{
|
||||
entry["value"] = *value;
|
||||
}
|
||||
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(
|
||||
{
|
||||
{"op", "add"}, {"path", path}, {"value", value}
|
||||
});
|
||||
diff_operation(result, "add", path, &value);
|
||||
}
|
||||
|
||||
/// @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 +7440,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 +7460,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7308,10 +7481,25 @@ public:
|
||||
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 +7512,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 +7583,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 +7606,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 +7615,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 +7624,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 +7634,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 +7646,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 +7667,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 +7676,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;
|
||||
}
|
||||
@@ -7492,6 +7700,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 +7708,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 +7847,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 +7870,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 +7880,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;
|
||||
}
|
||||
|
||||
@@ -723,8 +723,12 @@ class basic_json_view
|
||||
template<typename, bool> friend class basic_json_document;
|
||||
template<typename, bool> friend class basic_json_view;
|
||||
template<typename, typename> friend class detail::view::editor;
|
||||
template<typename, typename> friend class detail::view::view_side;
|
||||
friend iterator;
|
||||
|
||||
/// whether the view is one of an editable document
|
||||
static constexpr bool is_editable = Editable;
|
||||
|
||||
basic_json_view(const document_data* d, const node* n) noexcept
|
||||
: m_doc(d), m_node(n)
|
||||
{}
|
||||
|
||||
Generated
+1924
-594516
File diff suppressed because it is too large.
Load diff
Generated
+1443
-369
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
+1425
-462
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -13,6 +13,7 @@
|
||||
using nlohmann::json;
|
||||
|
||||
#include <array> // array
|
||||
#include <cctype> // toupper
|
||||
#include <cstdint> // uint32_t, uint64_t
|
||||
#include <cstdio> // snprintf
|
||||
#include <cmath> // isfinite
|
||||
@@ -599,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
|
||||
@@ -719,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
|
||||
|
||||
@@ -2452,6 +2452,36 @@ 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
|
||||
|
||||
@@ -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]})");
|
||||
@@ -274,6 +345,33 @@ TEST_CASE("json_view comparison")
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("objects whose first keys differ or repeat")
|
||||
{
|
||||
const char* const texts[] =
|
||||
{
|
||||
R"({"a":1,"b":2})", R"({"a":1,"c":2})", R"({"b":1,"a":2})", R"({"a":1,"a":2,"b":3})", R"({"a":2,"b":3})",
|
||||
R"({"a":1,"b":1,"a":2})", R"({"a":2,"b":1})", R"({"a":1,"a":2})", R"({"b":1})", R"({"a":1,"b":2,"c":3,"d":4,"e":5})",
|
||||
R"({"a":1,"b":2,"c":3,"d":4,"f":5})", R"({"a":1,"b":2,"c":3,"a":4,"e":5})", R"({"a":4,"b":2,"c":3,"e":5})",
|
||||
R"({"\u0061":1,"b":2})", R"({})", R"([{"a":1,"b":2}])", R"([{"a":1,"c":2}])"
|
||||
};
|
||||
for (const char* const x : texts)
|
||||
{
|
||||
for (const char* const y : texts)
|
||||
{
|
||||
CAPTURE(x)
|
||||
CAPTURE(y)
|
||||
const ordered_json_document dx = ordered_json_document::parse(x);
|
||||
const ordered_json_document dy = ordered_json_document::parse(y);
|
||||
const ordered_json_editable_document ex = ordered_json_editable_document::parse(x);
|
||||
const ordered_json jx = ordered_json::parse(x);
|
||||
const ordered_json jy = ordered_json::parse(y);
|
||||
CHECK((dx.root() == dy.root()) == (jx == jy));
|
||||
CHECK((dx.root() == jy) == (jx == jy));
|
||||
CHECK((ex.root() == dy.root()) == (jx == jy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("numbers, duplicate keys, member order")
|
||||
{
|
||||
const auto same = [](const char* x, const char* y)
|
||||
@@ -314,6 +412,129 @@ TEST_CASE("json_view comparison")
|
||||
CHECK(!(null_document.root() == discarded));
|
||||
}
|
||||
|
||||
SECTION("values that are the same text, or the same value written differently, at any depth")
|
||||
{
|
||||
// nested arrays and objects around depths where the walk over equal text gives up
|
||||
for (const int depth :
|
||||
{
|
||||
1, 5, 47, 48, 49, 50, 60, 300
|
||||
})
|
||||
{
|
||||
CAPTURE(depth)
|
||||
std::string head;
|
||||
std::string tail;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
{
|
||||
head += i % 2 == 0 ? R"({"k":[1,"s",{"x":true},)" : R"([null,2.5,{"y":)";
|
||||
tail = (i % 2 == 0 ? "]}" : "}]") + tail;
|
||||
}
|
||||
// the same text, one number changed, and one number written differently
|
||||
const std::string text = head + "7" + tail;
|
||||
const std::string changed = head + "8" + tail;
|
||||
const std::string spelled = head + "7.0" + tail;
|
||||
const json_document a = json_document::parse(text);
|
||||
const json_document b = json_document::parse(text);
|
||||
const json_document c = json_document::parse(changed);
|
||||
const json_document d = json_document::parse(spelled);
|
||||
CHECK(a.root() == b.root());
|
||||
CHECK(a.root() != c.root());
|
||||
CHECK(c.root() != a.root());
|
||||
CHECK(a.root() == d.root());
|
||||
CHECK(d.root() == a.root());
|
||||
CHECK(a.root() == json::parse(text));
|
||||
CHECK(a.root() != json::parse(changed));
|
||||
CHECK(json::parse(spelled) == a.root());
|
||||
const nlohmann::json_editable_document e = nlohmann::json_editable_document::parse(text);
|
||||
CHECK(a.root() == e.root());
|
||||
CHECK(e.root() == a.root());
|
||||
CHECK(c.root() != e.root());
|
||||
CHECK(e.root() == e.root());
|
||||
const ordered_json_document oa = ordered_json_document::parse(text);
|
||||
const ordered_json_document oc = ordered_json_document::parse(changed);
|
||||
CHECK(oa.root() != oc.root());
|
||||
CHECK(oa.root() == ordered_json::parse(text));
|
||||
CHECK(oc.root() != ordered_json::parse(text));
|
||||
}
|
||||
|
||||
// not the same text, but the same value
|
||||
const auto same = [](const char* x, const char* y)
|
||||
{
|
||||
const json_document dx = json_document::parse(x);
|
||||
const json_document dy = json_document::parse(y);
|
||||
const bool equal = dx.root() == dy.root();
|
||||
CHECK(equal == (json::parse(x) == json::parse(y)));
|
||||
CHECK(equal == (dx.root() == json::parse(y)));
|
||||
return equal;
|
||||
};
|
||||
CHECK(same(R"({"a":1,"a":2})", R"({"a":3,"a":2})"));
|
||||
CHECK(same(R"({"a":1,"b":[1,{"c":2,"c":3}]})", R"({"b":[1,{"c":9,"c":3}],"a":1})"));
|
||||
CHECK(!same(R"({"a":1,"a":2})", R"({"a":2,"a":3})"));
|
||||
CHECK(same("[1.0,-0,0,1e2]", "[1.00,0,-0,100.0]"));
|
||||
CHECK(same(R"(["\u00e9","\n"])", "[\"\xc3\xa9\",\"\\n\"]"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":false}]})"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"c":null}]})"));
|
||||
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":null,"c":1}]})"));
|
||||
|
||||
// edited documents: moved sequences, links, and values of new nodes
|
||||
nlohmann::json_editable_document edited = nlohmann::json_editable_document::parse(R"({"a":[1,2,3],"b":{"c":"d"},"e":0.5})");
|
||||
const json_document plain = json_document::parse(R"({"a":[1,2,3,4],"b":{"c":"x","y":null},"e":0.5,"z":[]})");
|
||||
edited.push_back(edited.root()["a"], 4);
|
||||
edited.set(edited.root()["b"], "c", "x");
|
||||
edited.set(edited.root()["b"], "y", nullptr);
|
||||
edited.set(edited.root(), "z", json::array());
|
||||
CHECK(edited.root() == plain.root());
|
||||
CHECK(plain.root() == edited.root());
|
||||
edited.erase(edited.root()["a"], 0);
|
||||
CHECK(edited.root() != plain.root());
|
||||
CHECK(edited.root()["a"] == json::parse("[2,3,4]"));
|
||||
const json_document tail = json_document::parse("[2,3,4]");
|
||||
CHECK(edited.root()["a"] == tail.root());
|
||||
}
|
||||
|
||||
SECTION("large documents, with a difference at any place")
|
||||
{
|
||||
const auto make = [](std::size_t count, std::size_t changed, bool swap_members)
|
||||
{
|
||||
std::string text = R"({"b":1,"list":[)";
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
text += (i != 0 ? "," : "");
|
||||
const std::string id = std::to_string(i == changed ? i + 1 : i);
|
||||
text += swap_members && i % 3 == 0 ? R"({"v":[1.5,"s",{"k":)" + id + R"(}],"id":)" + id + "}" : R"({"id":)" + id + R"(,"v":[1.5,"s",{"k":)" + id + "}]}";
|
||||
}
|
||||
return text + R"(],"a":{"y":2,"x":3}})";
|
||||
};
|
||||
const std::size_t count = 3000;
|
||||
for (const std::size_t changed :
|
||||
{
|
||||
count, std::size_t{0}, std::size_t{1}, count / 2, count - 1
|
||||
})
|
||||
{
|
||||
for (const bool swap :
|
||||
{
|
||||
false, true
|
||||
})
|
||||
{
|
||||
CAPTURE(changed)
|
||||
CAPTURE(swap)
|
||||
const std::string text_a = make(count, count, false);
|
||||
const std::string text_b = make(count, changed, swap);
|
||||
const json_document a = json_document::parse(text_a);
|
||||
const json_document b = json_document::parse(text_b);
|
||||
const bool equal = changed == count;
|
||||
CHECK((a.root() == b.root()) == equal);
|
||||
CHECK((b.root() == a.root()) == equal);
|
||||
CHECK((a.root() == json::parse(text_b)) == equal);
|
||||
CHECK((json::parse(text_b) == a.root()) == equal);
|
||||
CHECK((a.root()["list"] == b.root()["list"]) == equal);
|
||||
const ordered_json_document oa = ordered_json_document::parse(text_a);
|
||||
const ordered_json_document ob = ordered_json_document::parse(text_b);
|
||||
CHECK((oa.root() == ob.root()) == (equal && !swap));
|
||||
CHECK((oa.root() == ordered_json::parse(text_b)) == (equal && !swap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("deep nesting")
|
||||
{
|
||||
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
|
||||
|
||||
@@ -682,6 +682,66 @@ TEST_CASE("json_view images: ownership")
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
TEST_CASE("json_view images: errors")
|
||||
{
|
||||
#if !defined(JSON_NOEXCEPTION)
|
||||
SECTION("float tokens: a changed byte is accepted if the text is still a number with the digits the node records")
|
||||
{
|
||||
for (const char* const token :
|
||||
{
|
||||
"-12.375", "0.5", "123456.0", "-0.0", "9999999999999999999.5", "0.000000000000000000000001", "1.25e10", "5E-3"
|
||||
})
|
||||
{
|
||||
CAPTURE(token)
|
||||
const std::string text = std::string("[") + token + "]";
|
||||
const json_document d = json_document::parse(text);
|
||||
const std::vector<std::uint8_t> image = d.save();
|
||||
const std::size_t at = text_at(image) + 1; // the first byte of the token
|
||||
const std::uint16_t recorded = node_at(image, 1).extra;
|
||||
CHECK(loaded_dump(image) == json::parse(text).dump());
|
||||
for (std::size_t pos = 0; token[pos] != '\0'; ++pos)
|
||||
{
|
||||
for (unsigned int byte = 0; byte < 256; ++byte)
|
||||
{
|
||||
std::vector<std::uint8_t> changed = image;
|
||||
changed[at + pos] = static_cast<std::uint8_t>(byte);
|
||||
std::string t = token;
|
||||
t[pos] = static_cast<char>(byte);
|
||||
// the digits of the integer part and of the fraction of the changed token
|
||||
std::size_t integer = 0;
|
||||
std::size_t fraction = 0;
|
||||
std::size_t k = (t[0] == '-') ? 1 : 0;
|
||||
for (; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||
{
|
||||
++integer;
|
||||
}
|
||||
if (k < t.size() && t[k] == '.')
|
||||
{
|
||||
for (++k; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
|
||||
{
|
||||
++fraction;
|
||||
}
|
||||
}
|
||||
// (as a JSON text, the token may have white space around it)
|
||||
const bool float_token = json::accept(t) && t.find_first_of(std::string(" \t\r\n\0", 5)) == std::string::npos && (t.find('.') != std::string::npos || t.find_first_of("eE") != std::string::npos)
|
||||
&& ((integer | (fraction << 8U)) == recorded);
|
||||
CAPTURE(pos)
|
||||
CAPTURE(byte)
|
||||
std::string result;
|
||||
try
|
||||
{
|
||||
result = loaded_dump(changed);
|
||||
}
|
||||
catch (const json::parse_error& e)
|
||||
{
|
||||
CHECK(std::string(e.what()) == check_failed);
|
||||
result = "rejected";
|
||||
}
|
||||
CHECK((result != "rejected") == float_token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SECTION("a literal as the root: dump() after loading")
|
||||
{
|
||||
for (const char* text :
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | 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});
|
||||
CHECK(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);
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user