Compare commits

...

9 Commits

Author SHA1 Message Date
Daniel Lemire 263e58d9d7 guarding against zero memory case 2026-06-07 14:28:13 -04:00
Daniel Lemire ed345772cd we don't need this 2026-06-06 15:35:36 -04:00
Daniel Lemire 92123faa7d making perfect hash the default 2026-06-06 14:47:09 -04:00
Daniel Lemire 12518a278a minor fixes 2026-06-06 14:36:32 -04:00
Daniel Lemire dc9a8e2890 adding a new method... 2026-06-06 00:51:09 -04:00
Daniel Lemire 12062bc5bc simplifying. 2026-06-05 19:24:19 -04:00
Daniel Lemire 6e0abb7f9c lower level. 2026-06-03 00:32:41 -04:00
Daniel Lemire 686c9bff54 experimental 2026-06-02 23:39:47 -04:00
Daniel Lemire e1654be9b3 removing useless comment. 2026-06-01 20:57:12 -04:00
10 changed files with 298 additions and 126 deletions
+20 -32
View File
@@ -1528,10 +1528,6 @@ void f() {
```
**Performance tip**: You will get better performance if you order the attributes (make, model)
in the order they appear in the JSON document. Alternatively, [key selectors](#key-selectors)
let you extract a fixed, known set of fields in a single pass, independently of the
order in which they appear in the document.
#### Special cases
@@ -1684,29 +1680,23 @@ std::map<std::string, std::string> obj =
The `simdjson::from` construction is EXPERIMENTAL and subject to changes.
### Order-independent reflective deserialization (experimental)
### Order-independent reflective deserialization
> **Experimental and opt-in.** This feature is disabled by default. Enable it by
> defining the macro `SIMDJSON_USE_KEY_SELECTOR_REFLECTION=1` before including
> simdjson (e.g. as a compiler flag `-DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1`).
> It requires C++26 static reflection (`SIMDJSON_STATIC_REFLECTION`).
> **Default.** Reflective deserialization uses an order-independent, single-pass
> key-selector strategy by default. You can opt out — falling back to the
> per-member ordered-lookup path — by defining the macro
> `SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1` before including simdjson (e.g. as
> a compiler flag `-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1`). It requires
> C++26 static reflection (`SIMDJSON_STATIC_REFLECTION`).
By default, reflective deserialization (`doc.get<T>()` / `simdjson::from`) reads
each struct member with an ordered field lookup. This is fastest when the JSON
keys appear in the same order as the struct's members, which is the common case.
When you genuinely cannot rely on the JSON key order — for example when the data
comes from a producer that emits members in an arbitrary or varying order — the
ordered lookups degrade, because each out-of-order key forces a rescan of the
object. For that situation simdjson offers an optional, order-independent
strategy: when `SIMDJSON_USE_KEY_SELECTOR_REFLECTION=1` is defined, the
reflective deserializer builds a compile-time [key selector](#key-selectors)
from the struct's members and walks each object **once** with
`object::for_each`, classifying every key through a perfect hash regardless of
its position.
By default, reflective deserialization (`doc.get<T>()` / `simdjson::from`) builds
a compile-time [key selector](#key-selectors) from the struct's members and walks
each object **once** with `object::for_each`, classifying every key through a
perfect hash regardless of its position. This deserializes structs correctly even
when the JSON keys are not in declaration order, without forcing a rescan of the
object per out-of-order key.
```cpp
// Compile this translation unit with -DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1
#include "simdjson.h"
using namespace simdjson;
@@ -1721,17 +1711,15 @@ auto json = R"({ "retweet_count": 7, "id": 12345, "text": "hello" })"_padded;
ondemand::parser parser;
ondemand::document doc = parser.iterate(json);
Tweet t;
auto error = doc.get(t); // uses the key-selector path under the macro
auto error = doc.get(t); // uses the key-selector path by default
```
**This is not a universal speedup — run your own benchmarks before adopting it.**
In our measurements the key-selector path is roughly on par with the default on
small structs (e.g. the Twitter user/tweet objects) but **slower** on documents
dominated by many small nested objects (e.g. the CITM catalog), because walking
every field of every object and hashing each key costs more than ordered,
short-circuiting lookups when the keys *are* in order. Only consider enabling the
macro when (a) your inputs really do present keys out of order, and (b) your own
benchmarks on your own data show a win. Otherwise, leave it off.
The alternative, opt-out path reads each struct member with an ordered field
lookup. This can edge ahead when the JSON keys reliably match declaration order,
which is a common case. **Neither path is a universal speedup — run your own
benchmarks before switching.** Throughput numbers carry some run-to-run noise, so
measure on your own data before defining
`-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1`.
Minifying JSON strings without parsing
@@ -12,7 +12,6 @@
#include <string_view>
#include <cstddef>
#include <cstdint>
#include <cstring>
#if defined(__aarch64__) || defined(__ARM_NEON)
#include <arm_neon.h>
@@ -37,10 +36,7 @@ namespace key_selector_detail {
// ============================================================================
// Compile-time perfect-hash generator.
//
// This is a port of the ConstexprCore perfect-hash generator
// (https://github.com/ConstexprCore/perfect_hash). It scales to ~100 keys at
// compile time by determining association values one (position, character)
// It scales to ~100 keys at compile time by determining association values one (position, character)
// symbol at a time (gperf-style) instead of an exhaustive offset search, and
// falls back to a Hash-and-Displace construction for large/awkward key sets.
//
@@ -331,11 +327,9 @@ consteval bool try_generate_gperf(
std::array<std::size_t, N> phash{};
for (std::size_t k = 0; k < N; ++k) { phash[k] = keys[k].size(); }
// Equivalence-class signatures: sig[k] = XOR of a per-symbol salt over the
// key's undetermined symbols. Updated incrementally as symbols are fixed.
std::array<std::array<std::size_t, 256>, MAX_POSITIONS> salt{};
std::array<std::array<uint64_t, 256>, MAX_POSITIONS> salt{};
{
std::size_t s = 0x9e3779b97f4a7c15ULL;
uint64_t s = 0x9e3779b97f4a7c15ULL;
for (std::size_t p = 0; p < num_positions; ++p) {
for (std::size_t c = 0; c < 256; ++c) {
s = s * 6364136223846793005ULL + 1442695040888963407ULL;
@@ -343,9 +337,9 @@ consteval bool try_generate_gperf(
}
}
}
std::array<std::size_t, N> sig{};
std::array<uint64_t, N> sig{};
for (std::size_t k = 0; k < N; ++k) {
std::size_t s = 0;
uint64_t s = 0;
for (std::size_t p = 0; p < num_positions; ++p) {
std::size_t c = kchars[k][p];
if (c < 256) { s ^= salt[p][c]; }
@@ -365,14 +359,14 @@ consteval bool try_generate_gperf(
std::size_t sp = syms[si].pos;
std::size_t sc = syms[si].ch;
std::size_t sp_salt = salt[sp][sc];
uint64_t sp_salt = salt[sp][sc];
for (std::size_t k = 0; k < N; ++k) {
if (kchars[k][sp] == sc) { sig[k] ^= sp_salt; }
}
for (std::size_t i = 1; i < N; ++i) {
std::size_t x = order[i];
std::size_t xs = sig[x];
uint64_t xs = sig[x];
std::size_t j = i;
while (j > 0 && sig[order[j - 1]] > xs) {
order[j] = order[j - 1];
@@ -386,7 +380,7 @@ consteval bool try_generate_gperf(
bool collision = false;
std::size_t ci = 0;
while (ci < N && !collision) {
std::size_t class_sig = sig[order[ci]];
uint64_t class_sig = sig[order[ci]];
std::size_t cj = ci;
while (cj < N && sig[order[cj]] == class_sig) { ++cj; }
if (cj - ci > 1) {
@@ -762,7 +756,9 @@ simdjson_really_inline std::size_t scan_key_length(const char* p) noexcept {
template <std::size_t MaxKeyLen>
simdjson_really_inline bool compare_key_bytes(
const char* p, const char* stored, std::size_t len) noexcept {
alignas(16) static constexpr uint8_t idx16[16] =
// [[maybe_unused]]: only the NEON/SSE2 branches read these; the scalar
// fallback build (no SIMD) leaves them unused, which is an error under -Werror.
[[maybe_unused]] alignas(16) static constexpr uint8_t idx16[16] =
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
if constexpr (MaxKeyLen <= 16) {
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
@@ -787,7 +783,7 @@ simdjson_really_inline bool compare_key_bytes(
return true;
#endif
} else if constexpr (MaxKeyLen <= 32) {
alignas(16) static constexpr uint8_t idx32_hi[16] =
[[maybe_unused]] alignas(16) static constexpr uint8_t idx32_hi[16] =
{16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31};
#if SIMDJSON_KEY_SELECTOR_HAS_NEON
uint8x16_t vp_lo = vld1q_u8(reinterpret_cast<const uint8_t*>(p));
@@ -871,12 +867,16 @@ struct key_selector {
static constexpr std::size_t size() noexcept { return N; }
/**
* Look up a JSON key. rjs must point just after an opening quote in a padded
* simdjson buffer. Returns the selector index in [0, N) on match, or N on miss.
* Look up a JSON key whose length is already known. p must point at the first
* key byte (just after the opening quote) in a padded simdjson buffer, and len
* must be the number of raw key bytes (the distance to the closing quote).
* Returns the selector index in [0, N) on match, or N on miss.
*
* Prefer this overload when the caller can obtain the key length cheaply (for
* example, object::for_each derives it from the structural index rather than
* re-scanning for the closing quote).
*/
static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
const char* p = rjs.raw();
std::size_t len = key_selector_detail::scan_key_length<max_key_len>(p);
static simdjson_really_inline std::size_t match_raw(const char* p, std::size_t len) noexcept {
if (len == 0 || len > max_key_len) { return N; }
std::size_t slot;
@@ -915,6 +915,17 @@ struct key_selector {
return ki;
}
/**
* Look up a JSON key. rjs must point just after an opening quote in a padded
* simdjson buffer. Returns the selector index in [0, N) on match, or N on miss.
* The key length is recovered with a SIMD scan for the closing quote; callers
* that already know the length should use the (p, len) overload above.
*/
static simdjson_really_inline std::size_t match_raw(raw_json_string rjs) noexcept {
const char* p = rjs.raw();
return match_raw(p, key_selector_detail::scan_key_length<max_key_len>(p));
}
/** Return the key text at selector index i (i in [0, N)). */
static constexpr std::string_view key_at(std::size_t i) noexcept {
return keys[i];
+46 -27
View File
@@ -65,37 +65,56 @@ simdjson_inline simdjson_result<value> object::find_field(const std::string_view
#if SIMDJSON_SUPPORTS_CONCEPTS
template <typename Selector, typename Func>
simdjson_inline error_code object::for_each(Func&& on_match) noexcept {
auto first = this->begin();
if (first.error()) { return first.error(); }
object_iterator it = first.value_unsafe();
object_iterator last{};
std::array<bool, Selector::size()> seen{};
simdjson_flatten simdjson_inline for_each_result object::for_each(Func&& on_match) noexcept {
// Single pass driven directly by the value_iterator, mirroring
// find_field_unordered_raw + value(iter.child()). Compared to walking via
// object_iterator/field, this avoids constructing a simdjson_result<field> and
// a field (key + value) for every field -- and the development-check bookkeeping
// in object_iterator -- building a value only for the fields that actually match.
// We operate on a copy of the iterator, as object::begin() would.
value_iterator it = iter;
// Track which selector indices have already matched, as a compile-time bitset:
// a single 64-bit word for up to 64 keys (the common case), more words as
// needed. Initializing one (or a few) registers to zero is cheaper than zeroing
// a per-key byte array on every call, and the test/set become register bit ops.
constexpr std::size_t seen_words = (Selector::size() + 63) / 64;
std::array<std::uint64_t, seen_words> seen{};
std::size_t matched = 0;
while (it != last) {
auto field_res = *it;
if (field_res.error()) { return field_res.error(); }
field f = field_res.value_unsafe();
std::size_t idx = Selector::match_raw(f.key());
if (idx < Selector::size() && !seen[idx]) {
seen[idx] = true;
value matched_value = f.value();
// The callback may return either void or an error_code. When it returns an
// error_code, we stop at the first non-SUCCESS result and propagate it so
// the caller can surface value-parse errors (for example, a type mismatch
// on a matched field). A void-returning callback is responsible for
// handling its own errors.
if constexpr (std::is_same_v<decltype(on_match(idx, matched_value)), error_code>) {
error_code e = on_match(idx, matched_value);
if (e) { return e; }
} else {
on_match(idx, matched_value);
while (it.is_open()) {
raw_json_string key;
std::size_t key_len;
// field_key_with_length derives the key length from the structural index (the
// following ':' token), avoiding a forward SIMD scan for the closing quote.
error_code error = it.field_key_with_length(key, key_len);
if (error) { it.abandon(); return {error, matched}; }
// Advance past the ':' and descend onto the value.
if ((error = it.field_value())) { it.abandon(); return {error, matched}; }
std::size_t idx = Selector::match_raw(key.raw(), key_len);
if (idx < Selector::size()) {
const std::uint64_t seen_bit = std::uint64_t{1} << (idx & 63);
std::uint64_t &seen_word = seen[idx >> 6];
if (!(seen_word & seen_bit)) {
seen_word |= seen_bit;
value matched_value(it.child());
// The callback may return either void or an error_code. When it returns an
// error_code, we stop at the first non-SUCCESS result and propagate it so
// the caller can surface value-parse errors (for example, a type mismatch
// on a matched field). A void-returning callback is responsible for
// handling its own errors.
if constexpr (std::is_same_v<decltype(on_match(idx, matched_value)), error_code>) {
if ((error = on_match(idx, matched_value))) { return {error, matched}; }
} else {
on_match(idx, matched_value);
}
if (++matched >= Selector::size()) { break; }
}
if (++matched >= Selector::size()) { break; }
}
++it;
// Skip the value (a no-op if the callback consumed it) and step to the next
// field; has_next_field() ends the container on '}', which closes the loop.
if ((error = it.skip_child())) { it.abandon(); return {error, matched}; }
if ((error = it.has_next_field().error())) { return {error, matched}; }
}
return SUCCESS;
return {SUCCESS, matched};
}
#endif
+22 -3
View File
@@ -16,6 +16,22 @@ namespace simdjson {
namespace SIMDJSON_IMPLEMENTATION {
namespace ondemand {
#if SIMDJSON_SUPPORTS_CONCEPTS
/**
* Result of object::for_each: the first error encountered (SUCCESS if none) and
* the number of distinct selector keys that matched during the walk. A
* matched_count equal to Selector::size() means every selected key was present
* in the object. Implicitly converts to error_code so existing callers that only
* care about the error (including SIMDJSON_TRY and the test ASSERT_* macros) keep
* working unchanged.
*/
struct for_each_result {
error_code error{SUCCESS};
std::size_t matched_count{0};
constexpr operator error_code() const noexcept { return error; }
};
#endif
/**
* A forward-only JSON object field iterator.
*/
@@ -148,11 +164,14 @@ public:
* error_code, the walk stops at the first non-SUCCESS result and that error is
* returned, which lets the callback surface value-parse errors.
*
* @returns SUCCESS, or the first error encountered while walking the object
* (including any error returned by the callback).
* @returns a for_each_result holding the first error encountered while walking
* the object (including any error returned by the callback, SUCCESS if
* none) and the number of distinct selector keys that matched. The
* result converts implicitly to error_code, so callers that only need
* the error can ignore the count.
*/
template <typename Selector, typename Func>
simdjson_inline error_code for_each(Func&& on_match) noexcept;
simdjson_inline for_each_result for_each(Func&& on_match) noexcept;
#endif
/**
@@ -269,11 +269,12 @@ constexpr bool user_defined_type = (std::is_class_v<T>
!concepts::appendable_containers<T>);
#if defined(SIMDJSON_USE_KEY_SELECTOR_REFLECTION) && SIMDJSON_USE_KEY_SELECTOR_REFLECTION
#if !(defined(SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION) && SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION)
// Experimental: deserialize a reflected struct using a compile-time key_selector
// Default: deserialize a reflected struct using a compile-time key_selector
// and a single object::for_each pass (perfect-hash key matching) instead of one
// obj[key] lookup per member. Enable with -DSIMDJSON_USE_KEY_SELECTOR_REFLECTION=1.
// obj[key] lookup per member. Disable (falling back to the per-member obj[key]
// path below) with -DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1.
namespace key_selector_reflection_detail {
// A member participates if it is public, non-const, and not annotated to skip.
@@ -309,6 +310,37 @@ template <typename T>
using selector_for = typename [: std::meta::substitute(
^^SIMDJSON_IMPLEMENTATION::ondemand::key_selector, selector_key_args<T>()) :];
// Number of members that participate in deserialization. A class can have zero
// eligible members (e.g. std::chrono::time_point, whose only data member is
// private): an empty key_selector cannot be built, so the tag_invoke below
// special-cases this count.
template <typename T>
consteval std::size_t eligible_member_count() {
std::size_t count = 0;
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if constexpr (is_eligible_member(mem)) {
++count;
}
}
return count;
}
// True when none of T's eligible members is an optional type, i.e. every member
// is required. In that case presence can be checked with a single match count
// instead of a per-member "seen" array.
template <typename T>
consteval bool all_eligible_members_required() {
bool all_required = true;
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if constexpr (is_eligible_member(mem)) {
if constexpr (concepts::optional_type<typename [: std::meta::type_of(mem) :]>) {
all_required = false;
}
}
}
return all_required;
}
} // namespace key_selector_reflection_detail
template <typename T, typename ValT>
@@ -320,45 +352,80 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
} else {
SIMDJSON_TRY(val.get_object().get(obj));
}
if constexpr (key_selector_reflection_detail::eligible_member_count<T>() == 0) {
// No members to deserialize: an empty key_selector cannot be built, so just
// validate that the input is an object (done above) and succeed. Mirrors the
// opt-out per-member path, which iterates over zero members.
(void)out;
(void)obj;
return SUCCESS;
} else {
using selector = key_selector_reflection_detail::selector_for<T>;
std::array<bool, selector::size()> seen_member{};
// Single pass over the object: each field whose key matches a member yields its
// selector index, which we map back to the corresponding member. The callback
// returns an error_code so that a value-parse error (e.g. a type mismatch on a
// matched field) is propagated by for_each instead of being silently dropped.
error_code walk_error = obj.template for_each<selector>(
[&](std::size_t matched_index, SIMDJSON_IMPLEMENTATION::ondemand::value field_value) -> error_code {
std::size_t counter = 0;
error_code field_error = SUCCESS;
if constexpr (key_selector_reflection_detail::all_eligible_members_required<T>()) {
// Fast path: every member is required. A single for_each pass parses each
// matched field; the returned match count then tells us whether every member
// was present (matched_count == selector::size()) without a per-member "seen"
// array. A value-parse error (e.g. a type mismatch) is propagated by for_each.
auto walk = obj.template for_each<selector>(
[&](std::size_t matched_index, SIMDJSON_IMPLEMENTATION::ondemand::value field_value) -> error_code {
std::size_t counter = 0;
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
if (matched_index == counter) { return field_value.get(out.[:mem:]); }
++counter;
}
}
return SUCCESS;
});
if (walk.error) { return walk.error; }
// A missing required member shows up as a short match count and is reported as
// NO_SUCH_FIELD, mirroring the ordered obj[key] path.
if (walk.matched_count != selector::size()) { return NO_SUCH_FIELD; }
return SUCCESS;
} else {
std::array<bool, selector::size()> seen_member{};
// Single pass over the object: each field whose key matches a member yields its
// selector index, which we map back to the corresponding member. The callback
// returns an error_code so that a value-parse error (e.g. a type mismatch on a
// matched field) is propagated by for_each instead of being silently dropped.
error_code walk_error = obj.template for_each<selector>(
[&](std::size_t matched_index, SIMDJSON_IMPLEMENTATION::ondemand::value field_value) -> error_code {
std::size_t counter = 0;
error_code field_error = SUCCESS;
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
if (matched_index == counter) {
seen_member[counter] = true;
field_error = field_value.get(out.[:mem:]);
}
++counter;
}
}
return field_error;
});
if (walk_error) { return walk_error; }
// Required (non-optional) members must be present: a missing one is reported as
// NO_SUCH_FIELD, mirroring the ordered obj[key] path. Optional members may be
// absent.
std::size_t check_counter = 0;
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
if (matched_index == counter) {
seen_member[counter] = true;
field_error = field_value.get(out.[:mem:]);
if constexpr (!concepts::optional_type<decltype(out.[:mem:])>) {
if (!seen_member[check_counter]) { return NO_SUCH_FIELD; }
}
++counter;
++check_counter;
}
}
return field_error;
});
if (walk_error) { return walk_error; }
// Required (non-optional) members must be present: a missing one is reported as
// NO_SUCH_FIELD, mirroring the ordered obj[key] path. Optional members may be
// absent.
std::size_t check_counter = 0;
template for (constexpr auto mem : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if constexpr (key_selector_reflection_detail::is_eligible_member(mem)) {
if constexpr (!concepts::optional_type<decltype(out.[:mem:])>) {
if (!seen_member[check_counter]) { return NO_SUCH_FIELD; }
}
++check_counter;
}
return SUCCESS;
}
}
return SUCCESS;
}
#else
// Opt-out path (-DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1): deserialize a
// reflected struct with one obj[key] lookup per member, instead of the default
// single-pass object::for_each path above.
template <typename T, typename ValT>
requires(user_defined_type<T> && std::is_class_v<T>)
error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
@@ -391,7 +458,7 @@ error_code tag_invoke(deserialize_tag, ValT &val, T &out) noexcept {
return simdjson::SUCCESS;
}
#endif // SIMDJSON_USE_KEY_SELECTOR_REFLECTION
#endif // SIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION
// Support for enum deserialization - deserialize from string representation using expand approach from P2996R12
template <typename T, typename ValT>
@@ -423,6 +423,22 @@ simdjson_warn_unused simdjson_inline simdjson_result<raw_json_string> value_iter
return raw_json_string(key);
}
simdjson_warn_unused simdjson_inline error_code value_iterator::field_key_with_length(raw_json_string &key, std::size_t &len) noexcept {
assert_at_next();
const uint8_t *k = _json_iter->return_current_and_advance();
if (*(k++) != '"') { return report_error(TAPE_ERROR, "Object key is not a string"); }
// After return_current_and_advance(), the current token is the ':' that follows
// the key. The closing quote sits just before it (only JSON whitespace may
// intervene), so step back from the ':' to the closing quote to get the length.
// In minified JSON this is a single back-step.
const char *q = reinterpret_cast<const char *>(_json_iter->peek());
do { --q; } while (*q != '"');
key = raw_json_string(k);
len = static_cast<std::size_t>(q - reinterpret_cast<const char *>(k));
return SUCCESS;
}
simdjson_warn_unused simdjson_inline error_code value_iterator::field_value() noexcept {
assert_at_next();
@@ -158,6 +158,17 @@ public:
*/
simdjson_warn_unused simdjson_inline simdjson_result<raw_json_string> field_key() noexcept;
/**
* Get the current field's key together with its raw byte length.
*
* Like field_key(), but also returns the number of raw key bytes (the distance
* from the first key byte to the closing quote). The length is recovered from
* the structural index -- the next structural token is the ':' -- by stepping
* back over any whitespace to the closing quote, avoiding a forward SIMD scan
* for the closing quote. Leaves the iterator positioned exactly as field_key().
*/
simdjson_warn_unused simdjson_inline error_code field_key_with_length(raw_json_string &key, std::size_t &len) noexcept;
/**
* Pass the : in the field and move to its value.
*/
+3 -3
View File
@@ -38,9 +38,9 @@ add_cpp_test(ondemand_wrong_type_error_tests LABELS ondemand acceptance
add_cpp_test(ondemand_iterate_many_csv LABELS ondemand acceptance per_implementation)
add_cpp_test(ondemand_custom_types_tests LABELS ondemand acceptance per_implementation)
add_cpp_test(ondemand_custom_types_document_tests LABELS ondemand acceptance per_implementation)
# Reflective deserialization through the experimental key-selector path. The
# SIMDJSON_USE_KEY_SELECTOR_REFLECTION macro is defined at the top of the test
# source itself, so this is an ordinary test target.
# Reflective deserialization through the (default) key-selector path. No special
# macro is needed: the key-selector + object::for_each path is the default, so
# this is an ordinary test target.
add_cpp_test(ondemand_key_selector_reflection_tests LABELS ondemand acceptance per_implementation)
add_cpp_test(ondemand_stl_types_tests LABELS ondemand acceptance per_implementation)
add_cpp_test(ondemand_convert_tests LABELS ondemand acceptance per_implementation)
@@ -1,11 +1,10 @@
// Exercises the experimental key-selector-based reflective deserializer.
//
// We enable the option here (before including simdjson) so the reflective
// tag_invoke uses a compile-time key_selector + object::for_each single pass
// instead of one obj[key] lookup per member. The point of the option is to
// deserialize structs correctly even when the JSON keys are NOT in declaration
// order, so the tests below deliberately scramble the key order.
#define SIMDJSON_USE_KEY_SELECTOR_REFLECTION 1
// Exercises the key-selector-based reflective deserializer, which is the
// default for reflective tag_invoke: a compile-time key_selector +
// object::for_each single pass instead of one obj[key] lookup per member.
// (It can be disabled with -DSIMDJSON_DISABLE_KEY_SELECTOR_REFLECTION=1.) The
// point of this path is to deserialize structs correctly even when the JSON
// keys are NOT in declaration order, so the tests below deliberately scramble
// the key order.
#include "simdjson.h"
#include <optional>
#include <string>
@@ -344,6 +344,47 @@ namespace object_tests {
}
#endif
#if SIMDJSON_SUPPORTS_CONCEPTS
// The key_selector's matcher (match_raw, the perfect hash) must return the
// right selector index for every key, including tricky cases: keys that are
// prefixes of one another, keys that extend a real key, a long key, and misses.
// This guards the prefix/length disambiguation against the hash.
bool key_selector_matchers_agree() {
TEST_START();
// Probe builds "<key>\"" in a padded buffer and checks match_raw returns the
// expected selector index (or N for a miss).
auto probe = [](auto sel_tag, std::string_view key, std::size_t expected) -> bool {
using sel = decltype(sel_tag);
char buf[64] = {};
for (size_t i = 0; i < key.size(); ++i) { buf[i] = key[i]; }
buf[key.size()] = '"';
ondemand::raw_json_string r(reinterpret_cast<const uint8_t*>(buf));
ASSERT_EQUAL(sel::match_raw(r), expected);
return true;
};
// Selector with prefix keys and a 30-character key.
using small_sel = ondemand::key_selector<"a", "ab", "abc", "id", "name",
"abcdefghijklmnopqrstuvwxyz1234">;
std::size_t i = 0;
for (auto k : {"a", "ab", "abc", "id", "name", "abcdefghijklmnopqrstuvwxyz1234"}) {
if (!probe(small_sel{}, k, i++)) { return false; }
}
for (auto k : {"x", "abcd", "nam", "names", "i", "ids", "zzzzz", ""}) {
if (!probe(small_sel{}, k, small_sel::size())) { return false; }
}
// Larger selector exercising the same matcher.
using big_sel = ondemand::key_selector<"k00","k01","k02","k03","k04","k05",
"k06","k07","k08","k09","k10","k11">;
if (!probe(big_sel{}, "k00", 0)) { return false; }
if (!probe(big_sel{}, "k05", 5)) { return false; }
if (!probe(big_sel{}, "k11", 11)) { return false; }
for (auto k : {"k12","nope",""}) {
if (!probe(big_sel{}, k, big_sel::size())) { return false; }
}
TEST_SUCCEED();
}
#endif
bool run() {
return
object_find_field_unordered() &&
@@ -355,6 +396,7 @@ namespace object_tests {
#if SIMDJSON_SUPPORTS_CONCEPTS
object_find_field_key_selector() &&
object_for_each_callback_error() &&
key_selector_matchers_agree() &&
#endif
#if SIMDJSON_EXCEPTIONS && SIMDJSON_SUPPORTS_CONCEPTS
key_selector_example_toplevel() &&