From 7796f00dcfd09ca52140c4133ddc9bc197b832db Mon Sep 17 00:00:00 2001 From: Florin Malita Date: Fri, 8 Jun 2018 12:25:38 -0400 Subject: SkJson For now this is only wired to a bench and a couple of tests. Local numbers, for a ~500KB BM "compressed" json: micros bench 2456.54 json_rapidjson nonrendering 1192.38 json_skjson nonrendering Change-Id: I7b3514f84c7c525d1787722c43ad6095e3692563 Reviewed-on: https://skia-review.googlesource.com/127622 Reviewed-by: Mike Klein Commit-Queue: Florin Malita --- modules/skjson/BUILD.gn | 96 ++++ modules/skjson/include/SkJSON.h | 139 ++++++ modules/skjson/src/FuzzSkJSON.cpp | 32 ++ modules/skjson/src/SkJSON.cpp | 945 +++++++++++++++++++++++++++++++++++++ modules/skjson/src/SkJSONBench.cpp | 167 +++++++ modules/skjson/src/SkJSONTest.cpp | 272 +++++++++++ 6 files changed, 1651 insertions(+) create mode 100644 modules/skjson/BUILD.gn create mode 100644 modules/skjson/include/SkJSON.h create mode 100644 modules/skjson/src/FuzzSkJSON.cpp create mode 100644 modules/skjson/src/SkJSON.cpp create mode 100644 modules/skjson/src/SkJSONBench.cpp create mode 100644 modules/skjson/src/SkJSONTest.cpp (limited to 'modules') diff --git a/modules/skjson/BUILD.gn b/modules/skjson/BUILD.gn new file mode 100644 index 0000000000..9538c321d2 --- /dev/null +++ b/modules/skjson/BUILD.gn @@ -0,0 +1,96 @@ +# Copyright 2018 Google Inc. +# +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +declare_args() { + skia_enable_skjson = true +} + +config("public_config") { + if (skia_enable_skjson) { + defines = [ "SK_ENABLE_SKJSON" ] + include_dirs = [ "include" ] + } +} + +source_set("skjson") { + if (skia_enable_skjson) { + public_configs = [ ":public_config" ] + public = [ + "include/SkJSON.h", + ] + sources = [ + "src/SkJSON.cpp", + ] + configs += [ "../../:skia_private" ] + deps = [ + "../..:skia", + ] + } +} + +source_set("tests") { + if (skia_enable_skjson) { + testonly = true + + configs += [ + "../..:skia_private", + "../..:tests_config", + ] + sources = [ + "src/SkJSONTest.cpp", + ] + + deps = [ + "../..:gpu_tool_utils", + "../..:skia", + ] + + public_deps = [ + ":skjson", + ] + } +} + +source_set("bench") { + if (skia_enable_skjson) { + testonly = true + + configs += [ + "../..:bench_config", + "../..:skia_private", + ] + sources = [ + "src/SkJSONBench.cpp", + ] + + deps = [ + "../..:skia", + "../../third_party/rapidjson", + ] + + public_deps = [ + ":skjson", + ] + } +} + +source_set("fuzz") { + if (skia_enable_skjson) { + testonly = true + + configs += [ "../..:skia_private" ] + sources = [ + "src/FuzzSkJSON.cpp", + ] + + deps = [ + "../..:skia", + ] + + public_deps = [ + ":skjson", + ] + } +} diff --git a/modules/skjson/include/SkJSON.h b/modules/skjson/include/SkJSON.h new file mode 100644 index 0000000000..4cb312c64f --- /dev/null +++ b/modules/skjson/include/SkJSON.h @@ -0,0 +1,139 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkJSON_DEFINED +#define SkJSON_DEFINED + +#include "SkArenaAlloc.h" +#include "SkTypes.h" + +class SkWStream; + +namespace skjson { + +/** + * A fast and likely non-conforming JSON parser. + * + * Some known limitations/compromises: + * + * -- single-precision FP numbers + * + * -- missing string unescaping (no current users, could be easily added) + * + * + * Values are opaque, fixed-size (64 bits), immutable records. + * + * They can be freely converted to any of the facade types for type-specific functionality. + * + * Note: type checking is lazy/deferred, to facilitate chained property access - e.g. + * + * if (!v.as()["foo"].as()["bar"].is()) + * LOG("found v.foo.bar!"); + */ +class alignas(8) Value { +public: + enum class Type { + kNull, + kBool, + kNumber, + kString, + kArray, + kObject, + }; + + /** + * @return The public type of this record. + */ + Type getType() const; + + /** + * @return True if the record matches the facade type T. + */ + template + bool is() const { return T::IsType(this->getType()); } + + /** + * @return The record cast as facade type T. + * + * Note: this is always safe, as proper typing is enforced in the facade methods. + */ + template + const T& as() const { + return *reinterpret_cast(this->is() ? this : &Value::Null()); + } + + /** + * @return Null value singleton. + */ + static const Value& Null(); + +protected: + uint8_t fData8[8]; +}; + +class NullValue final : public Value { +public: + static bool IsType(Value::Type t) { return t == Type::kNull; } +}; + +template +class PrimitiveValue final : public Value { +public: + static bool IsType(Value::Type t) { return t == vtype; } + + T operator *() const; +}; + +template +class VectorValue : public Value { +public: + static bool IsType(Value::Type t) { return t == vtype; } + + size_t size() const; + + const T* begin() const; + const T* end() const; + + const T& operator[](size_t i) const { + return (i < this->size()) ? *(this->begin() + i) : T::Null(); + } +}; + +using BoolValue = PrimitiveValue; +using NumberValue = PrimitiveValue; +using StringValue = VectorValue; +using ArrayValue = VectorValue; + +struct Member { + StringValue fKey; + Value fValue; + + static const Member& Null(); +}; + +class ObjectValue final : public VectorValue { +public: + const Value& operator[](const char*) const; +}; + +class DOM final : public SkNoncopyable { +public: + explicit DOM(const char*); + + const Value& root() const { return *fRoot; } + + void write(SkWStream*) const; + +private: + SkArenaAlloc fAlloc; + const Value* fRoot; +}; + +} // namespace skjson + +#endif // SkJSON_DEFINED + diff --git a/modules/skjson/src/FuzzSkJSON.cpp b/modules/skjson/src/FuzzSkJSON.cpp new file mode 100644 index 0000000000..ce33cc1299 --- /dev/null +++ b/modules/skjson/src/FuzzSkJSON.cpp @@ -0,0 +1,32 @@ +/* + * Copyright 2018 Google, LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "SkAutoMalloc.h" +#include "SkData.h" +#include "SkJSON.h" +#include "SkStream.h" + +void FuzzSkJSON(sk_sp bytes) { + // TODO: add a size + len skjson::DOM factory? + SkAutoMalloc data(bytes->size() + 1); + auto* c_str = static_cast(data.get()); + + memcpy(c_str, bytes->data(), bytes->size()); + c_str[bytes->size()] = '\0'; + + skjson::DOM dom(c_str); + SkDynamicMemoryWStream wstream; + dom.write(&wstream); +} + +#if defined(IS_FUZZING_WITH_LIBFUZZER) +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + auto bytes = SkData::MakeWithoutCopy(data, size); + FuzzSkJSON(bytes); + return 0; +} +#endif diff --git a/modules/skjson/src/SkJSON.cpp b/modules/skjson/src/SkJSON.cpp new file mode 100644 index 0000000000..c9a4d7c51d --- /dev/null +++ b/modules/skjson/src/SkJSON.cpp @@ -0,0 +1,945 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "SkJSON.h" + +#include "SkStream.h" +#include "SkString.h" + +#include +#include + +namespace skjson { + +//#define SK_JSON_REPORT_ERRORS + +namespace { + +/* + Value's impl side: + + -- fixed 64-bit size + + -- 8-byte aligned + + -- union of: + + bool + int32 + float + char[8] (short string storage) + external payload pointer + + -- highest 3 bits reserved for type storage + + */ +static_assert( sizeof(Value) == 8, ""); +static_assert(alignof(Value) == 8, ""); + +static constexpr size_t kRecAlign = alignof(Value); + +// The current record layout assumes LE and will take some tweaking for BE. +#if defined(SK_CPU_BENDIAN) +static_assert(false, "Big-endian builds are not supported."); +#endif + +class ValueRec : public Value { +public: + static constexpr uint64_t kTypeBits = 3, + kTypeShift = 64 - kTypeBits, + kTypeMask = ((1ULL << kTypeBits) - 1) << kTypeShift; + + enum RecType : uint64_t { + // We picked kShortString == 0 so that tag 0b000 and stored max_size-size (7-7=0) + // conveniently overlap the '\0' terminator, allowing us to store a 7 character + // C string inline. + kShortString = 0b000ULL << kTypeShift, // inline payload + kNull = 0b001ULL << kTypeShift, // no payload + kBool = 0b010ULL << kTypeShift, // inline payload + kInt = 0b011ULL << kTypeShift, // inline payload + kFloat = 0b100ULL << kTypeShift, // inline payload + kString = 0b101ULL << kTypeShift, // ptr to external storage + kArray = 0b110ULL << kTypeShift, // ptr to external storage + kObject = 0b111ULL << kTypeShift, // ptr to external storage + }; + + RecType getRecType() const { + return static_cast(*this->cast() & kTypeMask); + } + + // Access the record data as T. + // + // This is also used to access the payload for inline records. Since the record type lives in + // the high bits, sizeof(T) must be less than sizeof(Value) when accessing inline payloads. + // + // E.g. + // + // uint8_t + // ----------------------------------------------------------------------- + // | val8 | val8 | val8 | val8 | val8 | val8 | val8 | TYPE| + // ----------------------------------------------------------------------- + // + // uint32_t + // ----------------------------------------------------------------------- + // | val32 | unused | TYPE| + // ----------------------------------------------------------------------- + // + // T* (64b) + // ----------------------------------------------------------------------- + // | T* (kTypeShift bits) |TYPE| + // ----------------------------------------------------------------------- + // + template + const T* cast() const { + static_assert(sizeof (T) <= sizeof(ValueRec), ""); + static_assert(alignof(T) <= alignof(ValueRec), ""); + return reinterpret_cast(this); + } + + template + T* cast() { return const_cast(const_cast(this)->cast()); } + + // Access the pointer payload. + template + const T* ptr() const { + static_assert(sizeof(uintptr_t) == sizeof(Value) || + sizeof(uintptr_t) * 2 == sizeof(Value), ""); + + return (sizeof(uintptr_t) < sizeof(Value)) + // For 32-bit, pointers are stored unmodified. + ? *this->cast() + // For 64-bit, we use the high bits of the pointer as type storage. + : reinterpret_cast(*this->cast() & ~kTypeMask); + } + + // Type-bound recs only store their type. + static ValueRec MakeTypeBound(RecType t) { + ValueRec v; + *v.cast() = t; + SkASSERT(v.getRecType() == t); + return v; + } + + // Primitive recs store a type and inline primitive payload. + template + static ValueRec MakePrimitive(RecType t, T src) { + ValueRec v = MakeTypeBound(t); + *v.cast() = src; + SkASSERT(v.getRecType() == t); + return v; + } + + // Pointer recs store a type (in the upper kTypeBits bits) and a pointer. + template + static ValueRec MakePtr(RecType t, const T* p) { + SkASSERT((t & kTypeMask) == t); + if (sizeof(uintptr_t) == sizeof(Value)) { + // For 64-bit, we rely on the pointer hi bits being unused. + SkASSERT(!(reinterpret_cast(p) & kTypeMask)); + } + + ValueRec v = MakeTypeBound(t); + *v.cast() |= reinterpret_cast(p); + + SkASSERT(v.getRecType() == t); + SkASSERT(v.ptr() == p); + + return v; + } + + // Vector recs point to externally allocated slabs with the following layout: + // + // [size_t n] [REC_0] ... [REC_n-1] [optional extra trailing storage] + // + // Long strings use extra_alloc_size == 1 to store the \0 terminator. + template + static ValueRec MakeVector(RecType t, const T* src, size_t size, SkArenaAlloc& alloc) { + // For zero-size arrays, we just store a nullptr. + size_t* size_ptr = nullptr; + + if (size) { + // The Ts are already in memory, so their size should be safeish. + const auto total_size = sizeof(size_t) + sizeof(T) * size + extra_alloc_size; + size_ptr = reinterpret_cast(alloc.makeBytesAlignedTo(total_size, kRecAlign)); + auto* data_ptr = reinterpret_cast(size_ptr + 1); + *size_ptr = size; + memcpy(data_ptr, src, sizeof(T) * size); + } + + return MakePtr(t, size_ptr); + } + + size_t vectorSize(RecType t) const { + if (this->is()) return 0; + SkASSERT(this->getRecType() == t); + + const auto* size_ptr = this->ptr(); + return size_ptr ? *size_ptr : 0; + } + + template + const T* vectorBegin(RecType t) const { + if (this->is()) return nullptr; + SkASSERT(this->getRecType() == t); + + const auto* size_ptr = this->ptr(); + return size_ptr ? reinterpret_cast(size_ptr + 1) : nullptr; + } + + template + const T* vectorEnd(RecType t) const { + if (this->is()) return nullptr; + SkASSERT(this->getRecType() == t); + + const auto* size_ptr = this->ptr(); + return size_ptr ? reinterpret_cast(size_ptr + 1) + *size_ptr : nullptr; + } + + // Strings have two flavors: + // + // -- short strings (len <= 7) -> these are stored inline, in the record + // (one byte reserved for null terminator/type): + // + // [str] [\0]|[max_len - actual_len] + // + // Storing [max_len - actual_len] allows the 'len' field to double-up as a + // null terminator when size == max_len (this works 'cause kShortString == 0). + // + // -- long strings (len > 7) -> these are externally allocated vectors (VectorRec). + // + // The string data plus a null-char terminator are copied over. + static constexpr size_t kMaxInlineStringSize = sizeof(Value) - 1; + + static ValueRec MakeString(const char* src, size_t size, SkArenaAlloc& alloc) { + ValueRec v; + + if (size > kMaxInlineStringSize) { + v = MakeVector(kString, src, size, alloc); + const_cast(v.vectorBegin(ValueRec::kString))[size] = '\0'; + } else { + v = MakeTypeBound(kShortString); + auto* payload = v.cast(); + memcpy(payload, src, size); + payload[size] = '\0'; + + const auto len_tag = SkTo(kMaxInlineStringSize - size); + // This technically overwrites the type hi bits, but is safe because + // 1) kShortString == 0 + // 2) 0 <= len_tag <= 7 + static_assert(kShortString == 0, "please don't break this"); + payload[kMaxInlineStringSize] = len_tag; + SkASSERT(v.getRecType() == kShortString); + } + return v; + } + + size_t stringSize() const { + if (this->getRecType() == ValueRec::kShortString) { + const auto* payload = this->cast(); + return kMaxInlineStringSize - SkToSizeT(payload[kMaxInlineStringSize]); + } + + return this->vectorSize(ValueRec::kString); + } + + const char* stringBegin() const { + if (this->getRecType() == ValueRec::kShortString) { + return this->cast(); + } + + return this->vectorBegin(ValueRec::kString); + } + + const char* stringEnd() const { + if (this->getRecType() == ValueRec::kShortString) { + const auto* payload = this->cast(); + return payload + kMaxInlineStringSize - SkToSizeT(payload[kMaxInlineStringSize]); + } + + return this->vectorEnd(ValueRec::kString); + } +}; + +} // namespace + + +// Boring public Value glue. + +const Value& Value::Null() { + static const Value g_null = ValueRec::MakeTypeBound(ValueRec::kNull); + return g_null; +} + +const Member& Member::Null() { + static const Member g_null = { Value::Null().as(), Value::Null() }; + return g_null; +} + +Value::Type Value::getType() const { + static constexpr Value::Type kTypeMap[] = { + Value::Type::kString, // kShortString + Value::Type::kNull, // kNull + Value::Type::kBool, // kBool + Value::Type::kNumber, // kInt + Value::Type::kNumber, // kFloat + Value::Type::kString, // kString + Value::Type::kArray, // kArray + Value::Type::kObject, // kObject + }; + + const auto& rec = *reinterpret_cast(this); + const auto type_index = static_cast(rec.getRecType() >> ValueRec::kTypeShift); + SkASSERT(type_index < SK_ARRAY_COUNT(kTypeMap)); + + return kTypeMap[type_index]; +} + +template <> +bool PrimitiveValue::operator*() const { + const auto& rec = *reinterpret_cast(this); + + if (rec.is()) return false; + + SkASSERT(rec.getRecType() == ValueRec::kBool); + + return *rec.cast(); +} + +template <> +double PrimitiveValue::operator*() const { + const auto& rec = *reinterpret_cast(this); + + if (rec.is()) return 0; + + SkASSERT(rec.getRecType() == ValueRec::kInt || + rec.getRecType() == ValueRec::kFloat); + + return rec.getRecType() == ValueRec::kInt + ? static_cast(*rec.cast()) + : static_cast(*rec.cast()); +} + +template <> +size_t VectorValue::size() const { + return reinterpret_cast(this)->vectorSize(ValueRec::kArray); +} + +template <> +const Value* VectorValue::begin() const { + return reinterpret_cast(this)->vectorBegin(ValueRec::kArray); +} + +template <> +const Value* VectorValue::end() const { + return reinterpret_cast(this)->vectorEnd(ValueRec::kArray); +} + +template <> +size_t VectorValue::size() const { + return reinterpret_cast(this)->vectorSize(ValueRec::kObject); +} + +template <> +const Member* VectorValue::begin() const { + return reinterpret_cast(this)->vectorBegin(ValueRec::kObject); +} + +template <> +const Member* VectorValue::end() const { + return reinterpret_cast(this)->vectorEnd(ValueRec::kObject); +} + +template <> +size_t VectorValue::size() const { + return reinterpret_cast(this)->stringSize(); +} + +template <> +const char* VectorValue::begin() const { + return reinterpret_cast(this)->stringBegin(); +} + +template <> +const char* VectorValue::end() const { + return reinterpret_cast(this)->stringEnd(); +} + +const Value& ObjectValue::operator[](const char* key) const { + // Reverse search for duplicates resolution (policy: return last). + const auto* begin = this->begin(); + const auto* member = this->end(); + + while (member > begin) { + --member; + if (0 == strcmp(key, member->fKey.as().begin())) { + return member->fValue; + } + } + + return Value::Null(); +} + +namespace { + +// Lexer/parser inspired by rapidjson [1], sajson [2] and pjson [3]. +// +// [1] https://github.com/Tencent/rapidjson/ +// [2] https://github.com/chadaustin/sajson +// [3] https://pastebin.com/hnhSTL3h + + +// bit 0 (0x01) - plain ASCII string character +// bit 1 (0x02) - whitespace +// bit 2 (0x04) - string terminator (" \0 [control chars]) +// bit 3 (0x08) - 0-9 +// bit 4 (0x10) - 0-9 e E . +static constexpr uint8_t g_token_flags[256] = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + 4, 4, 4, 4, 4, 4, 4, 4, 4, 6, 6, 4, 4, 6, 4, 4, // 0 + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, // 1 + 3, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0x11,1, // 2 + 0x19,0x19,0x19,0x19,0x19,0x19,0x19,0x19, 0x19,0x19, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 0x11,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 0x11,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + + // 128-255 + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 +}; + +static inline bool is_ws(char c) { return g_token_flags[static_cast(c)] & 0x02; } +static inline bool is_sterminator(char c) { return g_token_flags[static_cast(c)] & 0x04; } +static inline bool is_digit(char c) { return g_token_flags[static_cast(c)] & 0x08; } +static inline bool is_numeric(char c) { return g_token_flags[static_cast(c)] & 0x10; } + +static inline const char* skip_ws(const char* p) { + while (is_ws(*p)) ++p; + return p; +} + +static inline float pow10(int32_t exp) { + static constexpr float g_pow10_table[63] = + { + 1.e-031f, 1.e-030f, 1.e-029f, 1.e-028f, 1.e-027f, 1.e-026f, 1.e-025f, 1.e-024f, + 1.e-023f, 1.e-022f, 1.e-021f, 1.e-020f, 1.e-019f, 1.e-018f, 1.e-017f, 1.e-016f, + 1.e-015f, 1.e-014f, 1.e-013f, 1.e-012f, 1.e-011f, 1.e-010f, 1.e-009f, 1.e-008f, + 1.e-007f, 1.e-006f, 1.e-005f, 1.e-004f, 1.e-003f, 1.e-002f, 1.e-001f, 1.e+000f, + 1.e+001f, 1.e+002f, 1.e+003f, 1.e+004f, 1.e+005f, 1.e+006f, 1.e+007f, 1.e+008f, + 1.e+009f, 1.e+010f, 1.e+011f, 1.e+012f, 1.e+013f, 1.e+014f, 1.e+015f, 1.e+016f, + 1.e+017f, 1.e+018f, 1.e+019f, 1.e+020f, 1.e+021f, 1.e+022f, 1.e+023f, 1.e+024f, + 1.e+025f, 1.e+026f, 1.e+027f, 1.e+028f, 1.e+029f, 1.e+030f, 1.e+031f + }; + + static constexpr int32_t k_exp_offset = SK_ARRAY_COUNT(g_pow10_table) / 2; + + // We only support negative exponents for now. + SkASSERT(exp <= 0); + + return (exp >= -k_exp_offset) ? g_pow10_table[exp + k_exp_offset] + : std::pow(10.0f, static_cast(exp)); +} + +class DOMParser { +public: + explicit DOMParser(SkArenaAlloc& alloc) + : fAlloc(alloc) { + + fValueStack.reserve(kValueStackReserve); + fScopeStack.reserve(kScopeStackReserve); + } + + const Value& parse(const char* p) { + p = skip_ws(p); + + switch (*p) { + case '{': + goto match_object; + case '[': + goto match_array; + default: + return this->error(Value::Null(), p, "invalid top-level value"); + } + + match_object: + SkASSERT(*p == '{'); + p = skip_ws(p + 1); + + this->pushObjectScope(); + + if (*p == '}') goto pop_object; + + // goto match_object_key; + match_object_key: + p = skip_ws(p); + if (*p != '"') return this->error(Value::Null(), p, "expected object key"); + + p = this->matchString(p, [this](const char* key, size_t size) { + this->pushObjectKey(key, size); + }); + if (!p) return Value::Null(); + + p = skip_ws(p); + if (*p != ':') return this->error(Value::Null(), p, "expected ':' separator"); + + ++p; + + // goto match_value; + match_value: + p = skip_ws(p); + + switch (*p) { + case '\0': + return this->error(Value::Null(), p, "unexpected input end"); + case '"': + p = this->matchString(p, [this](const char* str, size_t size) { + this->pushString(str, size); + }); + break; + case '[': + goto match_array; + case 'f': + p = this->matchFalse(p); + break; + case 'n': + p = this->matchNull(p); + break; + case 't': + p = this->matchTrue(p); + break; + case '{': + goto match_object; + default: + p = this->matchNumber(p); + break; + } + + if (!p) return Value::Null(); + + // goto match_post_value; + match_post_value: + SkASSERT(!fScopeStack.empty()); + + p = skip_ws(p); + switch (*p) { + case ',': + ++p; + if (fScopeStack.back() >= 0) { + goto match_object_key; + } else { + goto match_value; + } + case ']': + goto pop_array; + case '}': + goto pop_object; + default: + return this->error(Value::Null(), p - 1, "unexpected value-trailing token"); + } + + // unreachable + SkASSERT(false); + + pop_object: + SkASSERT(*p == '}'); + + if (fScopeStack.back() < 0) { + return this->error(Value::Null(), p, "unexpected object terminator"); + } + + this->popObjectScope(); + + // goto pop_common + pop_common: + SkASSERT(*p == '}' || *p == ']'); + + ++p; + + if (fScopeStack.empty()) { + SkASSERT(fValueStack.size() == 1); + auto* root = fAlloc.make(); + *root = fValueStack.front(); + + // Stop condition: parsed the top level element and there is no trailing garbage. + return *skip_ws(p) == '\0' + ? *root + : this->error(Value::Null(), p, "trailing root garbage"); + } + + goto match_post_value; + + match_array: + SkASSERT(*p == '['); + p = skip_ws(p + 1); + + this->pushArrayScope(); + + if (*p != ']') goto match_value; + + // goto pop_array; + pop_array: + SkASSERT(*p == ']'); + + if (fScopeStack.back() >= 0) { + return this->error(Value::Null(), p, "unexpected array terminator"); + } + + this->popArrayScope(); + + goto pop_common; + + SkASSERT(false); + return Value::Null(); + } + + const SkString& getError() const { + return fError; + } + +private: + SkArenaAlloc& fAlloc; + + static constexpr size_t kValueStackReserve = 256; + static constexpr size_t kScopeStackReserve = 128; + std::vector fValueStack; + std::vector fScopeStack; + + SkString fError; + + template + void popScopeAsVec(ValueRec::RecType type, size_t scope_start) { + SkASSERT(scope_start > 0); + SkASSERT(scope_start <= fValueStack.size()); + + static_assert( sizeof(T) >= sizeof(Value), ""); + static_assert( sizeof(T) % sizeof(Value) == 0, ""); + static_assert(alignof(T) == alignof(Value), ""); + + const auto scope_count = fValueStack.size() - scope_start, + count = scope_count / (sizeof(T) / sizeof(Value)); + SkASSERT(scope_count % (sizeof(T) / sizeof(Value)) == 0); + + const auto* begin = reinterpret_cast(fValueStack.data() + scope_start); + + // Instantiate the placeholder value added in onPush{Object/Array}. + fValueStack[scope_start - 1] = ValueRec::MakeVector(type, begin, count, fAlloc); + + // Drop the current scope. + fScopeStack.pop_back(); + fValueStack.resize(scope_start); + } + + void pushObjectScope() { + // Object placeholder. + fValueStack.emplace_back(); + + // Object scope marker (size). + fScopeStack.push_back(SkTo(fValueStack.size())); + } + + void popObjectScope() { + const auto scope_start = fScopeStack.back(); + SkASSERT(scope_start > 0); + this->popScopeAsVec(ValueRec::kObject, SkTo(scope_start)); + + SkDEBUGCODE( + const auto& obj = fValueStack.back().as(); + SkASSERT(obj.is()); + for (const auto& member : obj) { + SkASSERT(member.fKey.is()); + } + ) + } + + void pushArrayScope() { + // Array placeholder. + fValueStack.emplace_back(); + + // Array scope marker (-size). + fScopeStack.push_back(-SkTo(fValueStack.size())); + } + + void popArrayScope() { + const auto scope_start = -fScopeStack.back(); + SkASSERT(scope_start > 0); + this->popScopeAsVec(ValueRec::kArray, SkTo(scope_start)); + + SkDEBUGCODE( + const auto& arr = fValueStack.back().as(); + SkASSERT(arr.is()); + ) + } + + void pushObjectKey(const char* key, size_t size) { + SkASSERT(fScopeStack.back() >= 0); + SkASSERT(fValueStack.size() >= SkTo(fScopeStack.back())); + SkASSERT(!((fValueStack.size() - SkTo(fScopeStack.back())) & 1)); + this->pushString(key, size); + } + + void pushTrue() { + fValueStack.push_back(ValueRec::MakePrimitive(ValueRec::kBool, true)); + } + + void pushFalse() { + fValueStack.push_back(ValueRec::MakePrimitive(ValueRec::kBool, false)); + } + + void pushNull() { + fValueStack.push_back(ValueRec::MakeTypeBound(ValueRec::kNull)); + } + + void pushString(const char* s, size_t size) { + fValueStack.push_back(ValueRec::MakeString(s, size, fAlloc)); + } + + void pushInt32(int32_t i) { + fValueStack.push_back(ValueRec::MakePrimitive(ValueRec::kInt, i)); + } + + void pushFloat(float f) { + fValueStack.push_back(ValueRec::MakePrimitive(ValueRec::kFloat, f)); + } + + template + const T& error(const T& ret_val, const char* p, const char* msg) { +#if defined(SK_JSON_REPORT_ERRORS) + static constexpr size_t kMaxContext = 128; + fError = SkStringPrintf("%s: >", msg); + fError.append(p, std::min(strlen(p), kMaxContext)); +#endif + return ret_val; + } + + const char* matchTrue(const char* p) { + SkASSERT(p[0] == 't'); + + if (p[1] == 'r' && p[2] == 'u' && p[3] == 'e') { + this->pushTrue(); + return p + 4; + } + + return this->error(nullptr, p, "invalid token"); + } + + const char* matchFalse(const char* p) { + SkASSERT(p[0] == 'f'); + + if (p[1] == 'a' && p[2] == 'l' && p[3] == 's' && p[4] == 'e') { + this->pushFalse(); + return p + 5; + } + + return this->error(nullptr, p, "invalid token"); + } + + const char* matchNull(const char* p) { + SkASSERT(p[0] == 'n'); + + if (p[1] == 'u' && p[2] == 'l' && p[3] == 'l') { + this->pushNull(); + return p + 4; + } + + return this->error(nullptr, p, "invalid token"); + } + + template + const char* matchString(const char* p, MatchFunc&& func) { + SkASSERT(*p == '"'); + const auto* s_begin = p + 1; + + // TODO: unescape + for (p = s_begin; !is_sterminator(*p); ++p) {} + + if (*p == '"') { + func(s_begin, p - s_begin); + return p + 1; + } + + return this->error(nullptr, s_begin - 1, "invalid string"); + } + + const char* matchFastFloatDecimalPart(const char* p, int sign, float f, int exp) { + SkASSERT(exp <= 0); + + for (;;) { + if (!is_digit(*p)) break; + f = f * 10.f + (*p++ - '0'); --exp; + if (!is_digit(*p)) break; + f = f * 10.f + (*p++ - '0'); --exp; + } + + if (is_numeric(*p)) { + SkASSERT(*p == '.' || *p == 'e' || *p == 'E'); + // We either have malformed input, or an (unsupported) exponent. + return nullptr; + } + + this->pushFloat(sign * f * pow10(exp)); + + return p; + } + + const char* matchFastFloatPart(const char* p, int sign, float f) { + for (;;) { + if (!is_digit(*p)) break; + f = f * 10.f + (*p++ - '0'); + if (!is_digit(*p)) break; + f = f * 10.f + (*p++ - '0'); + } + + if (!is_numeric(*p)) { + // Matched (integral) float. + this->pushFloat(sign * f); + return p; + } + + return (*p == '.') ? this->matchFastFloatDecimalPart(p + 1, sign, f, 0) + : nullptr; + } + + const char* matchFast32OrFloat(const char* p) { + int sign = 1; + if (*p == '-') { + sign = -1; + ++p; + } + + const auto* digits_start = p; + + int32_t n32 = 0; + + // This is the largest absolute int32 value we can handle before + // risking overflow *on the next digit* (214748363). + static constexpr int32_t kMaxInt32 = (std::numeric_limits::max() - 9) / 10; + + if (is_digit(*p)) { + n32 = (*p++ - '0'); + for (;;) { + if (!is_digit(*p) || n32 > kMaxInt32) break; + n32 = n32 * 10 + (*p++ - '0'); + } + } + + if (!is_numeric(*p)) { + // Did we actually match any digits? + if (p > digits_start) { + this->pushInt32(sign * n32); + return p; + } + return nullptr; + } + + if (*p == '.') { + const auto* decimals_start = ++p; + + int exp = 0; + + for (;;) { + if (!is_digit(*p) || n32 > kMaxInt32) break; + n32 = n32 * 10 + (*p++ - '0'); --exp; + if (!is_digit(*p) || n32 > kMaxInt32) break; + n32 = n32 * 10 + (*p++ - '0'); --exp; + } + + if (!is_numeric(*p)) { + // Did we actually match any digits? + if (p > decimals_start) { + this->pushFloat(sign * n32 * pow10(exp)); + return p; + } + return nullptr; + } + + if (n32 > kMaxInt32) { + // we ran out on n32 bits + return this->matchFastFloatDecimalPart(p, sign, n32, exp); + } + } + + return this->matchFastFloatPart(p, sign, n32); + } + + const char* matchNumber(const char* p) { + if (const auto* fast = this->matchFast32OrFloat(p)) return fast; + + // slow fallback + char* matched; + float f = strtof(p, &matched); + if (matched > p) { + this->pushFloat(f); + return matched; + } + return this->error(nullptr, p, "invalid numeric token"); + } +}; + +void Write(const Value& v, SkWStream* stream) { + switch (v.getType()) { + case Value::Type::kNull: + stream->writeText("null"); + break; + case Value::Type::kBool: + stream->writeText(*v.as() ? "true" : "false"); + break; + case Value::Type::kNumber: + stream->writeScalarAsText(*v.as()); + break; + case Value::Type::kString: + stream->writeText("\""); + stream->writeText(v.as().begin()); + stream->writeText("\""); + break; + case Value::Type::kArray: { + const auto& array = v.as(); + stream->writeText("["); + bool first_value = true; + for (const auto& v : array) { + if (!first_value) stream->writeText(","); + Write(v, stream); + first_value = false; + } + stream->writeText("]"); + break; + } + case Value::Type::kObject: + const auto& object = v.as(); + stream->writeText("{"); + bool first_member = true; + for (const auto& member : object) { + SkASSERT(member.fKey.getType() == Value::Type::kString); + if (!first_member) stream->writeText(","); + Write(member.fKey, stream); + stream->writeText(":"); + Write(member.fValue, stream); + first_member = false; + } + stream->writeText("}"); + break; + } +} + +} // namespace + +static constexpr size_t kMinChunkSize = 4096; + +DOM::DOM(const char* cstr) + : fAlloc(kMinChunkSize) { + DOMParser parser(fAlloc); + + fRoot = &parser.parse(cstr); +} + +void DOM::write(SkWStream* stream) const { + Write(*fRoot, stream); +} + +} // namespace skjson diff --git a/modules/skjson/src/SkJSONBench.cpp b/modules/skjson/src/SkJSONBench.cpp new file mode 100644 index 0000000000..4be3b972ab --- /dev/null +++ b/modules/skjson/src/SkJSONBench.cpp @@ -0,0 +1,167 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "Benchmark.h" +#include "SkData.h" +#include "SkJSON.h" +#include "SkStream.h" + +#if defined(SK_BUILD_FOR_ANDROID) +static constexpr const char* kBenchFile = "/data/local/tmp/bench.json"; +#else +static constexpr const char* kBenchFile = "/tmp/bench.json"; +#endif + +class JsonBench : public Benchmark { +public: + +protected: + const char* onGetName() override { return "json_skjson"; } + + bool isSuitableFor(Backend backend) override { return backend == kNonRendering_Backend; } + + void onPerCanvasPreDraw(SkCanvas*) override { + if (auto stream = SkStream::MakeFromFile(kBenchFile)) { + SkASSERT(stream->hasLength()); + fCStringData = SkData::MakeUninitialized(stream->getLength() + 1); + auto* data8 = reinterpret_cast(fCStringData->writable_data()); + SkAssertResult(stream->read(data8, stream->getLength()) == stream->getLength()); + data8[stream->getLength()] = '\0'; + + } else { + SkDebugf("!! Could not open bench file: %s\n", kBenchFile); + } + } + + void onPerCanvasPostDraw(SkCanvas*) override { + fCStringData = nullptr; + } + + void onDraw(int loops, SkCanvas*) override { + if (!fCStringData) return; + + for (int i = 0; i < loops; i++) { + skjson::DOM dom(static_cast(fCStringData->data())); + if (dom.root().is()) { + SkDebugf("!! Parsing failed.\n"); + return; + } + } + } + +private: + sk_sp fCStringData; + + using INHERITED = Benchmark; +}; + +DEF_BENCH( return new JsonBench; ) + +#if !defined(SK_BUILD_FOR_ANDROID_FRAMEWORK) + +#include "rapidjson/document.h" + +class RapidJsonBench : public Benchmark { +public: + +protected: + const char* onGetName() override { return "json_rapidjson"; } + + bool isSuitableFor(Backend backend) override { return backend == kNonRendering_Backend; } + + void onPerCanvasPreDraw(SkCanvas*) override { + if (auto stream = SkStream::MakeFromFile(kBenchFile)) { + SkASSERT(stream->hasLength()); + fCStringData = SkData::MakeUninitialized(stream->getLength() + 1); + auto* data8 = reinterpret_cast(fCStringData->writable_data()); + SkAssertResult(stream->read(data8, stream->getLength()) == stream->getLength()); + data8[stream->getLength()] = '\0'; + + } else { + SkDebugf("!! Could not open bench file: %s\n", kBenchFile); + } + } + + void onPerCanvasPostDraw(SkCanvas*) override { + fCStringData = nullptr; + } + + void onDraw(int loops, SkCanvas*) override { + if (!fCStringData) return; + + for (int i = 0; i < loops; i++) { + rapidjson::Document doc; + doc.Parse(static_cast(fCStringData->data())); + if (doc.HasParseError()) { + SkDebugf("!! Parsing failed.\n"); + return; + } + } + } + +private: + sk_sp fCStringData; + + using INHERITED = Benchmark; +}; + +DEF_BENCH( return new RapidJsonBench; ) + +#endif + +#if (0) + +#include "pjson.h" + +class PJsonBench : public Benchmark { +public: + +protected: + const char* onGetName() override { return "json_pjson"; } + + bool isSuitableFor(Backend backend) override { return backend == kNonRendering_Backend; } + + void onPerCanvasPreDraw(SkCanvas*) override { + if (auto stream = SkStream::MakeFromFile(kBenchFile)) { + SkASSERT(stream->hasLength()); + fCStringData = SkData::MakeUninitialized(stream->getLength() + 1); + auto* data8 = reinterpret_cast(fCStringData->writable_data()); + SkAssertResult(stream->read(data8, stream->getLength()) == stream->getLength()); + data8[stream->getLength()] = '\0'; + + } else { + SkDebugf("!! Could not open bench file: %s\n", kBenchFile); + } + } + + void onPerCanvasPostDraw(SkCanvas*) override { + fCStringData = nullptr; + } + + void onDraw(int loops, SkCanvas*) override { + if (!fCStringData) return; + + for (int i = 0; i < loops; i++) { + // Copy needed for in-place operation. + auto data = SkData::MakeWithCopy(fCStringData->data(), fCStringData->size()); + pjson::document doc; + if (!doc.deserialize_in_place(static_cast(data->writable_data()))) { + SkDebugf("!! Parsing failed.\n"); + return; + } + } + } + +private: + sk_sp fCStringData; + + using INHERITED = Benchmark; +}; + +DEF_BENCH( return new PJsonBench; ) + +#endif diff --git a/modules/skjson/src/SkJSONTest.cpp b/modules/skjson/src/SkJSONTest.cpp new file mode 100644 index 0000000000..8b876ddf4e --- /dev/null +++ b/modules/skjson/src/SkJSONTest.cpp @@ -0,0 +1,272 @@ +/* + * Copyright 2018 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "Test.h" + +#include "SkJSON.h" +#include "SkStream.h" + +using namespace skjson; + +DEF_TEST(SkJSON_Parse, reporter) { + static constexpr struct { + const char* in; + const char* out; + } g_tests[] = { + { "" , nullptr }, + { "[" , nullptr }, + { "]" , nullptr }, + { "{" , nullptr }, + { "}" , nullptr }, + { "{]" , nullptr }, + { "[}" , nullptr }, + { "1" , nullptr }, + { "true" , nullptr }, + { "false", nullptr }, + { "null" , nullptr }, + + { "[nulll]" , nullptr }, + { "[false2]", nullptr }, + { "[true:]" , nullptr }, + + { "[1 2]" , nullptr }, + { "[1,,2]" , nullptr }, + { "[1,2,]" , nullptr }, + { "[,1,2]" , nullptr }, + + { "[ \"foo" , nullptr }, + { "[ \"fo\0o\" ]" , nullptr }, + + { "{ null }" , nullptr }, + { "{ \"k\" : }" , nullptr }, + { "{ : null }" , nullptr }, + { "{ \"k\" : : null }" , nullptr }, + { "{ \"k\" : null , }" , nullptr }, + { "{ \"k\" : null \"k\" : 1 }", nullptr }, + + + { " \n\r\t [ \n\r\t ] \n\r\t " , "[]" }, + { "[ null ]" , "[null]" }, + { "[ true ]" , "[true]" }, + { "[ false ]" , "[false]" }, + { "[ 0 ]" , "[0]" }, + { "[ 1 ]" , "[1]" }, + { "[ 1.248 ]" , "[1.248]" }, + { "[ \"\" ]" , "[\"\"]" }, + { "[ \" f o o \" ]" , "[\" f o o \"]" }, + { "[ \"123456\" ]" , "[\"123456\"]" }, + { "[ \"1234567\" ]" , "[\"1234567\"]" }, + { "[ \"12345678\" ]" , "[\"12345678\"]" }, + { "[ \"123456789\" ]" , "[\"123456789\"]" }, + { "[ null , true, false,0,12.8 ]", "[null,true,false,0,12.8]" }, + + { " \n\r\t { \n\r\t } \n\r\t " , "{}" }, + { "{ \"k\" : null }" , "{\"k\":null}" }, + { "{ \"k1\" : null, \"k2 \":0 }", "{\"k1\":null,\"k2 \":0}" }, + { "{ \"k1\" : null, \"k1\":0 }" , "{\"k1\":null,\"k1\":0}" }, + + { "{ \"k1\" : null, \n\ + \"k2\" : 0, \n\ + \"k3\" : [ \n\ + true, \r\n\ + { \"kk1\" : \"foo\" , \n\ + \"kk2\" : \"bar\" , \n\ + \"kk3\" : 1.28 , \n\ + \"kk4\" : [ 42 ] \n\ + } , \n\ + \"boo\" , \n\ + null \n\ + ] \n\ + }", + "{\"k1\":null,\"k2\":0,\"k3\":[true," + "{\"kk1\":\"foo\",\"kk2\":\"bar\",\"kk3\":1.28,\"kk4\":[42]},\"boo\",null]}" }, + }; + + for (const auto& tst : g_tests) { + DOM dom(tst.in); + const auto success = !dom.root().is(); + REPORTER_ASSERT(reporter, success == (tst.out != nullptr)); + if (!success) continue; + + SkDynamicMemoryWStream str; + dom.write(&str); + str.write8('\0'); + + auto data = str.detachAsData(); + REPORTER_ASSERT(reporter, !strcmp(tst.out, static_cast(data->data()))); + } + +} + +template +static void check_primitive(skiatest::Reporter* reporter, const Value& v, T pv, + bool is_type) { + + REPORTER_ASSERT(reporter, v.is() == is_type); + REPORTER_ASSERT(reporter, *v.as() == pv); +} + +template +static void check_vector(skiatest::Reporter* reporter, const Value& v, size_t expected_size, + bool is_vector) { + REPORTER_ASSERT(reporter, v.is() == is_vector); + + const auto& vec = v.as(); + REPORTER_ASSERT(reporter, vec.size() == expected_size); + REPORTER_ASSERT(reporter, (vec.begin() != nullptr) == is_vector); + REPORTER_ASSERT(reporter, vec.end() == vec.begin() + expected_size); +} + +static void check_string(skiatest::Reporter* reporter, const Value& v, const char* s) { + check_vector(reporter, v, s ? strlen(s) : 0, !!s); + if (s) { + REPORTER_ASSERT(reporter, !strcmp(v.as().begin(), s)); + } +} + +DEF_TEST(SkJSON_DOM, reporter) { + static constexpr char json[] = "{ \n\ + \"k1\": null, \n\ + \"k2\": false, \n\ + \"k3\": true, \n\ + \"k4\": 42, \n\ + \"k5\": .75, \n\ + \"k6\": \"foo\", \n\ + \"k7\": [ 1, true, \"bar\" ], \n\ + \"k8\": { \"kk1\": 2, \"kk2\": false, \"kk1\": \"baz\" } \n\ + }"; + + DOM dom(json); + + const auto& jroot = dom.root().as(); + REPORTER_ASSERT(reporter, jroot.is()); + + { + const auto& v = jroot["k1"]; + REPORTER_ASSERT(reporter, v.is()); + + check_primitive(reporter, v, false, false); + check_primitive(reporter, v, 0, false); + + check_string(reporter, v, nullptr); + check_vector(reporter, v, 0, false); + check_vector(reporter, v, 0, false); + } + + { + const auto& v = jroot["k2"]; + REPORTER_ASSERT(reporter, !v.is()); + + check_primitive(reporter, v, false, true); + check_primitive(reporter, v, 0, false); + + check_string(reporter, v, nullptr); + check_vector(reporter, v, 0, false); + check_vector(reporter, v, 0, false); + } + + { + const auto& v = jroot["k3"]; + REPORTER_ASSERT(reporter, !v.is()); + + check_primitive(reporter, v, true, true); + check_primitive(reporter, v, 0, false); + + check_string(reporter, v, nullptr); + check_vector(reporter, v, 0, false); + check_vector(reporter, v, 0, false); + } + + { + const auto& v = jroot["k4"]; + REPORTER_ASSERT(reporter, !v.is()); + + check_primitive(reporter, v, false, false); + check_primitive(reporter, v, 42, true); + + check_string(reporter, v, nullptr); + check_vector(reporter, v, 0, false); + check_vector(reporter, v, 0, false); + } + + { + const auto& v = jroot["k5"]; + REPORTER_ASSERT(reporter, !v.is()); + + check_primitive(reporter, v, false, false); + check_primitive(reporter, v, .75f, true); + + check_string(reporter, v, nullptr); + check_vector(reporter, v, 0, false); + check_vector(reporter, v, 0, false); + } + + { + const auto& v = jroot["k6"]; + REPORTER_ASSERT(reporter, !v.is()); + + check_primitive(reporter, v, false, false); + check_primitive(reporter, v, 0, false); + + check_string(reporter, v, "foo"); + check_vector(reporter, v, 0, false); + check_vector(reporter, v, 0, false); + } + + { + const auto& v = jroot["k7"]; + REPORTER_ASSERT(reporter, !v.is()); + + check_primitive(reporter, v, false, false); + check_primitive(reporter, v, 0, false); + + check_string(reporter, v, nullptr); + check_vector(reporter, v, 0, false); + + check_vector(reporter, v, 3, true); + check_primitive(reporter, v.as()[0], 1, true); + check_primitive(reporter, v.as()[1], true, true); + check_vector(reporter, v.as()[2], 3, true); + REPORTER_ASSERT(reporter, v.as()[3].is()); + } + + { + const auto& v = jroot["k8"]; + REPORTER_ASSERT(reporter, !v.is()); + + check_primitive(reporter, v, false, false); + check_primitive(reporter, v, 0, false); + + check_string(reporter, v, nullptr); + check_vector(reporter, v, 0, false); + + check_vector(reporter, v, 3, true); + + const auto& m0 = v.as().begin()[0]; + check_string(reporter, m0.fKey, "kk1"); + check_primitive(reporter, m0.fValue, 2, true); + + const auto& m1 = v.as().begin()[1]; + check_string(reporter, m1.fKey, "kk2"); + check_primitive(reporter, m1.fValue, false, true); + + const auto& m2 = v.as().begin()[2]; + check_string(reporter, m2.fKey, "kk1"); + check_string(reporter, m2.fValue, "baz"); + + REPORTER_ASSERT(reporter, v.as()[""].is()); + REPORTER_ASSERT(reporter, v.as()["nosuchkey"].is()); + check_string(reporter, v.as()["kk1"], "baz"); + check_primitive(reporter, v.as()["kk2"], false, true); + } + + { + const auto& v = + jroot["foo"].as()["bar"].as()["baz"]; + REPORTER_ASSERT(reporter, v.is()); + } +} -- cgit v1.2.3