diff --git a/BUILD.bazel b/BUILD.bazel index 92f0149c5..1c9b27269 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -81,6 +81,7 @@ cc_library( "include/nlohmann/detail/view/pointer.hpp", "include/nlohmann/detail/view/scan.hpp", "include/nlohmann/detail/view/serializer.hpp", + "include/nlohmann/detail/view/simd.hpp", "include/nlohmann/detail/view/string_ref.hpp", "include/nlohmann/detail/view/value.hpp", "include/nlohmann/json.hpp", diff --git a/README.md b/README.md index 487e9ce55..b9e771c97 100644 --- a/README.md +++ b/README.md @@ -1405,6 +1405,7 @@ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR I - The class contains parts of [Google Abseil](https://github.com/abseil/abseil-cpp) which is licensed under the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0). - The class contains an adapted version of the Eisel-Lemire algorithm and its table of powers of five from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors - The view's parser (``) contains techniques and code adapted from [yyjson](https://github.com/ibireme/yyjson) by YaoYuan, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above): table-driven decoding of `\u` escapes and fixed-offset unrolled checks. +- The view's parser (``) validates non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright © 2018-2025 The simdjson authors REUSE Software diff --git a/docs/docset/docSet.sql b/docs/docset/docSet.sql index 3d3c22a02..f45936b27 100644 --- a/docs/docset/docSet.sql +++ b/docs/docset/docSet.sql @@ -307,3 +307,5 @@ INSERT INTO searchIndex(name, type, path) VALUES ('NLOHMANN_JSON_SERIALIZE_ENUM' INSERT INTO searchIndex(name, type, path) VALUES ('NLOHMANN_JSON_VERSION_MAJOR', 'Macro', 'api/macros/nlohmann_json_version_major/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('NLOHMANN_JSON_VERSION_MINOR', 'Macro', 'api/macros/nlohmann_json_version_major/index.html'); INSERT INTO searchIndex(name, type, path) VALUES ('NLOHMANN_JSON_VERSION_PATCH', 'Macro', 'api/macros/nlohmann_json_version_major/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('JSON_VIEW_NO_SIMD', 'Macro', 'api/macros/json_view_no_simd/index.html'); +INSERT INTO searchIndex(name, type, path) VALUES ('JSON_VIEW_USE_SSSE3', 'Macro', 'api/macros/json_view_use_ssse3/index.html'); diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index bf773b5c4..8a04910b4 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -33,6 +33,8 @@ header. See also the [macro overview page](../../features/macros.md). - [**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 - [**JSON_USE_SIMDUTF**](json_use_simdutf.md) - use the simdutf library to accelerate UTF-8 validation +- [**JSON_VIEW_NO_SIMD**](json_view_no_simd.md) - use only portable code in the parser of `json_view.hpp` +- [**JSON_VIEW_USE_SSSE3**](json_view_use_ssse3.md) - validate non-ASCII strings with SSSE3 in the parser of `json_view.hpp` ## Library version diff --git a/docs/mkdocs/docs/api/macros/json_view_no_simd.md b/docs/mkdocs/docs/api/macros/json_view_no_simd.md new file mode 100644 index 000000000..daa982760 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_view_no_simd.md @@ -0,0 +1,50 @@ +# JSON_VIEW_NO_SIMD + +```cpp +#define JSON_VIEW_NO_SIMD +``` + +When defined, the parser of [`basic_json_document`](../basic_json_document/index.md) (``) +uses only portable C++ to scan strings. By default, it scans long runs of string bytes 16 at a time with NEON on +AArch64 (with GCC and Clang) and SSE2 on x86-64, which are part of the baseline instruction sets of these +architectures, and validates non-ASCII text with NEON (or SSSE3, see +[`JSON_VIEW_USE_SSSE3`](json_view_use_ssse3.md)). + +The same input is accepted or rejected either way, with the same values, and errors are reported the same way; only +the speed differs. The macro exists for platforms whose compilers lack the intrinsics headers, and to test the portable +code. + +!!! warning "Define consistently" + + The macro selects between two definitions of the same inline functions. It must therefore be defined identically for + **every** translation unit that includes ``; prefer a compile definition on the target. + +## Default definition + +By default, `#!cpp JSON_VIEW_NO_SIMD` is not defined, and the vector code is used where available. + +```cpp +#undef JSON_VIEW_NO_SIMD +``` + +## Examples + +??? example + + The code below uses the portable string scanning of the view. + + ```cpp + #define JSON_VIEW_NO_SIMD + #include + + ... + ``` + +## See also + +- [JSON_VIEW_USE_SSSE3](json_view_use_ssse3.md) - validate non-ASCII strings with SSSE3 on x86-64 +- [json_view](../../features/json_view.md) - the zero-copy view + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/macros/json_view_use_ssse3.md b/docs/mkdocs/docs/api/macros/json_view_use_ssse3.md new file mode 100644 index 000000000..f76f96732 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_view_use_ssse3.md @@ -0,0 +1,49 @@ +# JSON_VIEW_USE_SSSE3 + +```cpp +#define JSON_VIEW_USE_SSSE3 +``` + +When defined on x86-64, the parser of [`basic_json_document`](../basic_json_document/index.md) +(``) validates non-ASCII text in strings with SSSE3, 16 bytes at a time, using the "lookup4" +algorithm of [simdjson](https://github.com/simdjson/simdjson). Without it, non-ASCII text is validated one UTF-8 +sequence at a time on x86-64; on AArch64, the vector check uses NEON and is always on. + +SSSE3 is not part of the x86-64 baseline, so the code must be compiled for it: define the macro only together with a +compiler option that enables SSSE3 (e.g. `-mssse3`, or `-march=` with a CPU that has it), and only for programs that +run on such CPUs. The same input is accepted or rejected either way; only the speed of non-ASCII text differs. + +!!! warning "Define consistently" + + The macro selects between two definitions of the same inline functions. It must therefore be defined identically, + with the same compiler options, for **every** translation unit that includes ``; mixing + translation units that define it with ones that do not is an ODR violation. Prefer a compile definition on the + target. + +## Default definition + +By default, `#!cpp JSON_VIEW_USE_SSSE3` is not defined. + +```cpp +#undef JSON_VIEW_USE_SSSE3 +``` + +## Examples + +??? example + + With CMake, for a program that only runs on CPUs with SSSE3: + + ```cmake + target_compile_definitions(your_target PRIVATE JSON_VIEW_USE_SSSE3) + target_compile_options(your_target PRIVATE -mssse3) + ``` + +## See also + +- [JSON_VIEW_NO_SIMD](json_view_no_simd.md) - use only portable code in the view's parser +- [JSON_USE_SIMDUTF](json_use_simdutf.md) - validate UTF-8 with simdutf in `basic_json`'s parser + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/home/license.md b/docs/mkdocs/docs/home/license.md index 9e602777c..327c7bc1a 100644 --- a/docs/mkdocs/docs/home/license.md +++ b/docs/mkdocs/docs/home/license.md @@ -23,3 +23,5 @@ The class contains a copy of [Hedley](https://nemequ.github.io/hedley/) from Eva The class contains an adapted version of the Eisel-Lemire algorithm and its table of powers of five from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright © 2021 The fast_float authors The view's parser (``) contains techniques and code adapted from [yyjson](https://github.com/ibireme/yyjson) by YaoYuan, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above): table-driven decoding of `\u` escapes and fixed-offset unrolled checks. + +The view's parser (``) validates non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright © 2018-2025 The simdjson authors diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 69cbf5c16..d47429af1 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -368,6 +368,8 @@ nav: - 'JSON_USE_IMPLICIT_CONVERSIONS': api/macros/json_use_implicit_conversions.md - 'JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON': api/macros/json_use_legacy_discarded_value_comparison.md - 'JSON_USE_SIMDUTF': api/macros/json_use_simdutf.md + - 'JSON_VIEW_NO_SIMD': api/macros/json_view_no_simd.md + - 'JSON_VIEW_USE_SSSE3': api/macros/json_view_use_ssse3.md - 'NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE, NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE, NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE, NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE': api/macros/nlohmann_define_derived_type.md - 'NLOHMANN_DEFINE_TYPE_INTRUSIVE, NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE': api/macros/nlohmann_define_type_intrusive.md - 'NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE': api/macros/nlohmann_define_type_non_intrusive.md diff --git a/include/nlohmann/detail/view/builder.hpp b/include/nlohmann/detail/view/builder.hpp index 45f547072..7856fb642 100644 --- a/include/nlohmann/detail/view/builder.hpp +++ b/include/nlohmann/detail/view/builder.hpp @@ -492,7 +492,7 @@ class builder switch (cur()) \ { \ case '"': \ - if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ + if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ goto NEXT; \ case '{': \ open(value_t::object); \ @@ -576,7 +576,7 @@ obj_key: { return fail(error_code::expected_key); } - if (NLOHMANN_VIEW_UNLIKELY(!string())) + if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } @@ -679,7 +679,7 @@ root_done: switch (cur()) { case '"': - return string(); + return string(); case 't': return literal("true", 4, value_t::boolean, node_flags::is_true); case 'f': @@ -965,12 +965,13 @@ indent_done: return true; } - /// a string (value or key) at p + /// a string at p: a value (Value) or a key + template NLOHMANN_VIEW_ALWAYS_INLINE bool string() { ++p; // opening quote const unsigned char* const s = p; - p = scan_string_run(p, e); + p = scan_string_run(p, e); if (NLOHMANN_VIEW_LIKELY(p != e && *p == '"')) { emit(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); diff --git a/include/nlohmann/detail/view/macro_unscope.hpp b/include/nlohmann/detail/view/macro_unscope.hpp index 11f71ad1b..b41a18381 100644 --- a/include/nlohmann/detail/view/macro_unscope.hpp +++ b/include/nlohmann/detail/view/macro_unscope.hpp @@ -19,3 +19,8 @@ #undef NLOHMANN_VIEW_THROW #undef NLOHMANN_VIEW_LITTLE_ENDIAN #undef NLOHMANN_VIEW_REPEAT16 +#undef NLOHMANN_VIEW_NEON +#undef NLOHMANN_VIEW_SSE2 +#undef NLOHMANN_VIEW_SSSE3 +#undef NLOHMANN_VIEW_VECTOR +#undef NLOHMANN_VIEW_VECTOR_UTF8 diff --git a/include/nlohmann/detail/view/scan.hpp b/include/nlohmann/detail/view/scan.hpp index 6d732ae36..c81f3c891 100644 --- a/include/nlohmann/detail/view/scan.hpp +++ b/include/nlohmann/detail/view/scan.hpp @@ -16,6 +16,7 @@ #include #include +#include // Scanning primitives of the view's parser. The unrolled checks at fixed // offsets follow yyjson (https://github.com/ibireme/yyjson, MIT license): the @@ -62,9 +63,12 @@ NLOHMANN_VIEW_ALWAYS_INLINE std::uint16_t load16(const unsigned char* p) noexcep /// Advance over plain string bytes and well-formed UTF-8. Stops at a quote, /// a backslash, a control character, ill-formed UTF-8, or the end. The first -/// 16 bytes are checked one by one, so that the position advances by -/// constants in predicted branches (most strings are short); longer runs -/// continue eight bytes at a time. +/// bytes are checked one by one, so that the position advances by constants +/// in predicted branches: 16 for keys, whose lengths repeat from record to +/// record, and 8 for string values (Value) where a vector loop follows, as +/// their lengths vary more. Longer runs continue 16 bytes at a time with NEON +/// or SSE2, else eight bytes at a time. +template NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned char* p, const unsigned char* e) noexcept { const std::uint8_t* plain = string_plain(); @@ -73,9 +77,23 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned if (e - p >= 16) { #define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { p += (i); goto stop; } - NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) + NLOHMANN_VIEW_STEP(0) NLOHMANN_VIEW_STEP(1) NLOHMANN_VIEW_STEP(2) NLOHMANN_VIEW_STEP(3) + NLOHMANN_VIEW_STEP(4) NLOHMANN_VIEW_STEP(5) NLOHMANN_VIEW_STEP(6) NLOHMANN_VIEW_STEP(7) + if (!Value || !NLOHMANN_VIEW_VECTOR) + { + NLOHMANN_VIEW_STEP(8) NLOHMANN_VIEW_STEP(9) NLOHMANN_VIEW_STEP(10) NLOHMANN_VIEW_STEP(11) + NLOHMANN_VIEW_STEP(12) NLOHMANN_VIEW_STEP(13) NLOHMANN_VIEW_STEP(14) NLOHMANN_VIEW_STEP(15) + p += 8; + } #undef NLOHMANN_VIEW_STEP - p += 16; + p += 8; +#if NLOHMANN_VIEW_VECTOR + p = vector_plain_run(p, e); + if (p != e && plain[*p] == 0) + { + goto stop; + } +#else while (e - p >= 8) { const std::uint64_t special = swar_string_special(read_eight_bytes(p)); @@ -86,6 +104,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned } p += 8; } +#endif continue; } while (p != e && plain[*p] != 0) @@ -101,6 +120,10 @@ stop: { return p; // quote, backslash, or control character } +#if NLOHMANN_VIEW_VECTOR_UTF8 + // non-ASCII: the vector check, out of line + return scan_string_vector(p, e, plain); +#else // non-ASCII: a run of well-formed sequences (the library's check, so // that exactly what json::parse accepts is accepted) do @@ -113,6 +136,7 @@ stop: p += n; } while (p != e && *p >= 0x80); +#endif } } diff --git a/include/nlohmann/detail/view/simd.hpp b/include/nlohmann/detail/view/simd.hpp new file mode 100644 index 000000000..5ca46c0f6 --- /dev/null +++ b/include/nlohmann/detail/view/simd.hpp @@ -0,0 +1,274 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2018-2025 The simdjson authors +// SPDX-License-Identifier: MIT + +#pragma once + +#include // size_t +#include // uint8_t, uint64_t + +#include +#include + +// Vector code for long runs of string bytes. 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 if JSON_VIEW_USE_SSSE3 is defined: SSSE3 is +// not part of x86-64, so it 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). JSON_VIEW_NO_SIMD selects the +// portable code. +#if !defined(JSON_VIEW_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN + #include + #define NLOHMANN_VIEW_NEON 1 +#else + #define NLOHMANN_VIEW_NEON 0 +#endif +#if !defined(JSON_VIEW_NO_SIMD) && !NLOHMANN_VIEW_NEON && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) + #include + #define NLOHMANN_VIEW_SSE2 1 +#else + #define NLOHMANN_VIEW_SSE2 0 +#endif +#if NLOHMANN_VIEW_SSE2 && defined(JSON_VIEW_USE_SSSE3) + #include + #define NLOHMANN_VIEW_SSSE3 1 +#else + #define NLOHMANN_VIEW_SSSE3 0 +#endif +#define NLOHMANN_VIEW_VECTOR (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSE2) +#define NLOHMANN_VIEW_VECTOR_UTF8 (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSSE3) + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +#if NLOHMANN_VIEW_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. +*/ +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#if NLOHMANN_VIEW_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(static_cast(p))); + 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(static_cast(_mm_movemask_epi8(special))); + if (bits != 0) + { + return p + count_trailing_zeros(bits); + } +#endif + p += 16; + } + return p; +} +#endif + +#if NLOHMANN_VIEW_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 +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::uint8_t byte_1_high[16]; + static const std::uint8_t byte_1_low[16]; + static const std::uint8_t byte_2_high[16]; +}; + +template +const std::uint8_t utf8_lookup4::byte_1_high[16] = +{ + 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 +const std::uint8_t utf8_lookup4::byte_1_low[16] = +{ + 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 +const std::uint8_t utf8_lookup4::byte_2_high[16] = +{ + too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short, + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4), + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + too_short, too_short, too_short, too_short +}; + +/// 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, const std::uint8_t* plain) 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 = c >= 0xF0 ? 4 : (c >= 0xE0 ? 3 : 2); + if (len > i) + { + block -= i; + } + break; + } + } + for (p = block; p != e;) + { + if (*p < 0x80) + { + if (plain[*p] == 0) + { + return p; + } + ++p; + continue; + } + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + return p; +} + +/*! +@brief the rest of a string from 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 covers the bytes up to it. Returns where the +string scan stops, like scan_string_run: before ill-formed UTF-8 and for the +last bytes of the input, the bytes are checked one sequence at a time. Out of +line, so that no constants of the check occupy registers in the parse loop. +*/ +NLOHMANN_VIEW_NOINLINE inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e, const std::uint8_t* plain) noexcept +{ + using lookup = utf8_lookup4<>; + const unsigned char* block = p; +#if NLOHMANN_VIEW_NEON + const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high); + const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low); + const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high); + 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(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) + { + return block + k; + } + 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(static_cast(lookup::byte_1_high))); + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low))); + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high))); + 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(static_cast(block))); + 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(-128))), sc); + const auto special_bits = static_cast(_mm_movemask_epi8(special)); + const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; + if (special_bits != 0) + { + const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + if ((err_bits & ((2u << k) - 1u)) == 0) + { + return block + k; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#endif + return scan_string_finish(p, block, e, plain); +} +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END diff --git a/single_include/nlohmann/json_view.hpp b/single_include/nlohmann/json_view.hpp index 393b52c75..cdef393b0 100644 --- a/single_include/nlohmann/json_view.hpp +++ b/single_include/nlohmann/json_view.hpp @@ -395,6 +395,283 @@ NLOHMANN_JSON_NAMESPACE_END // #include // #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-FileCopyrightText: 2018-2025 The simdjson authors +// SPDX-License-Identifier: MIT + + + +#include // size_t +#include // uint8_t, uint64_t + +// #include +// #include + + +// Vector code for long runs of string bytes. 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 if JSON_VIEW_USE_SSSE3 is defined: SSSE3 is +// not part of x86-64, so it 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). JSON_VIEW_NO_SIMD selects the +// portable code. +#if !defined(JSON_VIEW_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN + #include + #define NLOHMANN_VIEW_NEON 1 +#else + #define NLOHMANN_VIEW_NEON 0 +#endif +#if !defined(JSON_VIEW_NO_SIMD) && !NLOHMANN_VIEW_NEON && (defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)) + #include + #define NLOHMANN_VIEW_SSE2 1 +#else + #define NLOHMANN_VIEW_SSE2 0 +#endif +#if NLOHMANN_VIEW_SSE2 && defined(JSON_VIEW_USE_SSSE3) + #include + #define NLOHMANN_VIEW_SSSE3 1 +#else + #define NLOHMANN_VIEW_SSSE3 0 +#endif +#define NLOHMANN_VIEW_VECTOR (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSE2) +#define NLOHMANN_VIEW_VECTOR_UTF8 (NLOHMANN_VIEW_NEON || NLOHMANN_VIEW_SSSE3) + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +namespace view +{ + +#if NLOHMANN_VIEW_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. +*/ +NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* vector_plain_run(const unsigned char* p, const unsigned char* e) noexcept +{ + while (e - p >= 16) + { +#if NLOHMANN_VIEW_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(static_cast(p))); + 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(static_cast(_mm_movemask_epi8(special))); + if (bits != 0) + { + return p + count_trailing_zeros(bits); + } +#endif + p += 16; + } + return p; +} +#endif + +#if NLOHMANN_VIEW_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 +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::uint8_t byte_1_high[16]; + static const std::uint8_t byte_1_low[16]; + static const std::uint8_t byte_2_high[16]; +}; + +template +const std::uint8_t utf8_lookup4::byte_1_high[16] = +{ + 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 +const std::uint8_t utf8_lookup4::byte_1_low[16] = +{ + 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 +const std::uint8_t utf8_lookup4::byte_2_high[16] = +{ + too_short, too_short, too_short, too_short, too_short, too_short, too_short, too_short, + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large_1000 | overlong_4), + static_cast(too_long | overlong_2 | two_conts | overlong_3 | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + static_cast(too_long | overlong_2 | two_conts | surrogate | too_large), + too_short, too_short, too_short, too_short +}; + +/// 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, const std::uint8_t* plain) 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 = c >= 0xF0 ? 4 : (c >= 0xE0 ? 3 : 2); + if (len > i) + { + block -= i; + } + break; + } + } + for (p = block; p != e;) + { + if (*p < 0x80) + { + if (plain[*p] == 0) + { + return p; + } + ++p; + continue; + } + const std::size_t n = validate_one_utf8(p, static_cast(e - p)); + if (n == 0) + { + return p; + } + p += n; + } + return p; +} + +/*! +@brief the rest of a string from 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 covers the bytes up to it. Returns where the +string scan stops, like scan_string_run: before ill-formed UTF-8 and for the +last bytes of the input, the bytes are checked one sequence at a time. Out of +line, so that no constants of the check occupy registers in the parse loop. +*/ +NLOHMANN_VIEW_NOINLINE inline const unsigned char* scan_string_vector(const unsigned char* p, const unsigned char* e, const std::uint8_t* plain) noexcept +{ + using lookup = utf8_lookup4<>; + const unsigned char* block = p; +#if NLOHMANN_VIEW_NEON + const uint8x16_t t1h = vld1q_u8(lookup::byte_1_high); + const uint8x16_t t1l = vld1q_u8(lookup::byte_1_low); + const uint8x16_t t2h = vld1q_u8(lookup::byte_2_high); + 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(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) + { + return block + k; + } + 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(static_cast(lookup::byte_1_high))); + const __m128i t1l = _mm_loadu_si128(static_cast(static_cast(lookup::byte_1_low))); + const __m128i t2h = _mm_loadu_si128(static_cast(static_cast(lookup::byte_2_high))); + 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(static_cast(block))); + 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(-128))), sc); + const auto special_bits = static_cast(_mm_movemask_epi8(special)); + const auto err_bits = ~static_cast(_mm_movemask_epi8(_mm_cmpeq_epi8(err, zero))) & 0xFFFFu; + if (special_bits != 0) + { + const unsigned k = static_cast(count_trailing_zeros(static_cast(special_bits))); + if ((err_bits & ((2u << k) - 1u)) == 0) + { + return block + k; + } + break; + } + if (err_bits != 0) + { + break; + } + prev = in; + block += 16; + } +#endif + return scan_string_finish(p, block, e, plain); +} +#endif + +} // namespace view +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // Scanning primitives of the view's parser. The unrolled checks at fixed // offsets follow yyjson (https://github.com/ibireme/yyjson, MIT license): the @@ -441,9 +718,12 @@ NLOHMANN_VIEW_ALWAYS_INLINE std::uint16_t load16(const unsigned char* p) noexcep /// Advance over plain string bytes and well-formed UTF-8. Stops at a quote, /// a backslash, a control character, ill-formed UTF-8, or the end. The first -/// 16 bytes are checked one by one, so that the position advances by -/// constants in predicted branches (most strings are short); longer runs -/// continue eight bytes at a time. +/// bytes are checked one by one, so that the position advances by constants +/// in predicted branches: 16 for keys, whose lengths repeat from record to +/// record, and 8 for string values (Value) where a vector loop follows, as +/// their lengths vary more. Longer runs continue 16 bytes at a time with NEON +/// or SSE2, else eight bytes at a time. +template NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned char* p, const unsigned char* e) noexcept { const std::uint8_t* plain = string_plain(); @@ -452,9 +732,23 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned if (e - p >= 16) { #define NLOHMANN_VIEW_STEP(i) if (NLOHMANN_VIEW_LIKELY(plain[p[i]] != 0)) {} else { p += (i); goto stop; } - NLOHMANN_VIEW_REPEAT16(NLOHMANN_VIEW_STEP) + NLOHMANN_VIEW_STEP(0) NLOHMANN_VIEW_STEP(1) NLOHMANN_VIEW_STEP(2) NLOHMANN_VIEW_STEP(3) + NLOHMANN_VIEW_STEP(4) NLOHMANN_VIEW_STEP(5) NLOHMANN_VIEW_STEP(6) NLOHMANN_VIEW_STEP(7) + if (!Value || !NLOHMANN_VIEW_VECTOR) + { + NLOHMANN_VIEW_STEP(8) NLOHMANN_VIEW_STEP(9) NLOHMANN_VIEW_STEP(10) NLOHMANN_VIEW_STEP(11) + NLOHMANN_VIEW_STEP(12) NLOHMANN_VIEW_STEP(13) NLOHMANN_VIEW_STEP(14) NLOHMANN_VIEW_STEP(15) + p += 8; + } #undef NLOHMANN_VIEW_STEP - p += 16; + p += 8; +#if NLOHMANN_VIEW_VECTOR + p = vector_plain_run(p, e); + if (p != e && plain[*p] == 0) + { + goto stop; + } +#else while (e - p >= 8) { const std::uint64_t special = swar_string_special(read_eight_bytes(p)); @@ -465,6 +759,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* scan_string_run(const unsigned } p += 8; } +#endif continue; } while (p != e && plain[*p] != 0) @@ -480,6 +775,10 @@ stop: { return p; // quote, backslash, or control character } +#if NLOHMANN_VIEW_VECTOR_UTF8 + // non-ASCII: the vector check, out of line + return scan_string_vector(p, e, plain); +#else // non-ASCII: a run of well-formed sequences (the library's check, so // that exactly what json::parse accepts is accepted) do @@ -492,6 +791,7 @@ stop: p += n; } while (p != e && *p >= 0x80); +#endif } } @@ -1036,7 +1336,7 @@ class builder switch (cur()) \ { \ case '"': \ - if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ + if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } \ goto NEXT; \ case '{': \ open(value_t::object); \ @@ -1120,7 +1420,7 @@ obj_key: { return fail(error_code::expected_key); } - if (NLOHMANN_VIEW_UNLIKELY(!string())) + if (NLOHMANN_VIEW_UNLIKELY(!string())) { return false; } @@ -1223,7 +1523,7 @@ root_done: switch (cur()) { case '"': - return string(); + return string(); case 't': return literal("true", 4, value_t::boolean, node_flags::is_true); case 'f': @@ -1509,12 +1809,13 @@ indent_done: return true; } - /// a string (value or key) at p + /// a string at p: a value (Value) or a key + template NLOHMANN_VIEW_ALWAYS_INLINE bool string() { ++p; // opening quote const unsigned char* const s = p; - p = scan_string_run(p, e); + p = scan_string_run(p, e); if (NLOHMANN_VIEW_LIKELY(p != e && *p == '"')) { emit(value_t::string, 0, 0, static_cast(s - b), static_cast(p - s)); @@ -4747,6 +5048,11 @@ class tuple_element> // NOLINT(cert #undef NLOHMANN_VIEW_THROW #undef NLOHMANN_VIEW_LITTLE_ENDIAN #undef NLOHMANN_VIEW_REPEAT16 +#undef NLOHMANN_VIEW_NEON +#undef NLOHMANN_VIEW_SSE2 +#undef NLOHMANN_VIEW_SSSE3 +#undef NLOHMANN_VIEW_VECTOR +#undef NLOHMANN_VIEW_VECTOR_UTF8 #endif // INCLUDE_NLOHMANN_JSON_VIEW_HPP_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8c81b9123..66a5cb18c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -324,6 +324,26 @@ json_test_add_test_for(src/unit-diagnostic-positions.cpp MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} ) +# the json_view parser again with the portable string scanning instead of +# NEON/SSE2, and on x86-64 with the SSSE3 UTF-8 check (JSON_VIEW_USE_SSSE3) +json_test_set_test_options(test-json_view_builder_portable + COMPILE_DEFINITIONS JSON_VIEW_NO_SIMD +) +json_test_add_test_for(src/unit-json_view_builder.cpp + NAME test-json_view_builder_portable + 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-json_view_builder_ssse3 + COMPILE_DEFINITIONS JSON_VIEW_USE_SSSE3 + COMPILE_OPTIONS -mssse3 + ) + json_test_add_test_for(src/unit-json_view_builder.cpp + NAME test-json_view_builder_ssse3 + MAIN test_main CXX_STANDARDS ${test_cxx_standards} ${test_force} + ) +endif() + # *DO NOT* use json_test_set_test_options() below this line ############################################################################# diff --git a/tests/src/unit-json_view_builder.cpp b/tests/src/unit-json_view_builder.cpp index 4584e8728..e0b1d4393 100644 --- a/tests/src/unit-json_view_builder.cpp +++ b/tests/src/unit-json_view_builder.cpp @@ -136,6 +136,32 @@ void check_same(const std::string& text) } } +// a string value and a key must be accepted or rejected as json::parse does, +// and give its value (cheaper than check_same: the options do not matter) +void check_string(const std::string& content) +{ + for (const std::string& text : + { + "[\"" + content + "\"]", "{\"" + content + "\":1}" + }) + { + const bool accepted = json::accept(text); + for (const bool sentinel : + { + true, false + }) + { + const built b = build(text, false, false, sentinel); + if (b.ok != accepted || (accepted && value_of(b) != json::parse(text))) + { + CAPTURE(text); + CHECK(b.ok == accepted); + CHECK((b.ok && accepted ? value_of(b) == json::parse(text) : true)); + } + } + } +} + // a small deterministic generator of documents struct generator { @@ -364,3 +390,81 @@ TEST_CASE("json_view builder") } } } + +TEST_CASE("json_view builder: strings across vector blocks") +{ + // Strings are scanned 8 or 16 bytes at a time (NEON, SSE2, or SWAR) and + // non-ASCII text with the vector UTF-8 check (NEON, SSSE3) or one sequence + // at a time. Sequences are placed so that they start at every offset + // around the block boundaries of keys (16, 32) and values (8, 24), with + // text of several lengths after them. + const std::size_t prefixes[] = {0, 6, 7, 8, 13, 14, 15, 16, 21, 22, 23, 24, 29, 30, 31, 32}; + const std::size_t suffixes[] = {0, 3, 17}; + const auto around = [&](const std::string & seq, std::size_t prefix, std::size_t suffix) + { + return std::string(prefix, 'a') + seq + std::string(suffix, 'b'); + }; + + SECTION("every two-byte sequence") + { + for (unsigned lead = 0x80; lead <= 0xFF; ++lead) + { + for (unsigned second = 0; second <= 0xFF; ++second) + { + if (second == '"' || second == '\\') + { + continue; + } + const std::string seq = {static_cast(lead), static_cast(second)}; + check_string(around(seq, prefixes[(lead + second) % 16], suffixes[second % 3])); + } + } + } + + SECTION("three- and four-byte sequences") + { + const unsigned conts[] = {0x7F, 0x80, 0x8F, 0x90, 0x9F, 0xA0, 0xBF, 0xC0}; + for (unsigned lead = 0xE0; lead <= 0xF7; ++lead) + { + for (const unsigned b2 : conts) + { + for (const unsigned b3 : conts) + { + for (const std::size_t prefix : prefixes) + { + std::string seq = {static_cast(lead), static_cast(b2), static_cast(b3)}; + if (lead >= 0xF0) + { + seq += static_cast(prefix % 2 == 0 ? 0x80 : 0xBF); + } + check_string(around(seq, prefix, suffixes[prefix % 3])); + // cut short before the end of the string + check_string(around(seq.substr(0, seq.size() - 1), prefix, suffixes[prefix % 3])); + } + } + } + } + } + + SECTION("long runs of text with one damaged byte") + { + const char* const chars[] = {"a", "\xc3\xa9", "\xe3\x81\x82", "\xf0\x9f\x98\x80", "\xed\x9f\xbf", "\xef\xbf\xbf", "\xf4\x8f\xbf\xbf"}; + const char damage[] = {'\x80', '\xbf', '\xc0', '\xc1', '\xe0', '\xed', '\xf5', '\xff', '\x1f', '"', '\\'}; + std::mt19937 rng(5295); + for (int i = 0; i < 4000; ++i) + { + std::string text; + const auto n = rng() % 60; + for (unsigned k = 0; k < n; ++k) + { + text += chars[rng() % 7]; + } + check_string(text); + if (!text.empty()) + { + text[rng() % text.size()] = damage[rng() % 11]; + check_string(text); + } + } + } +}