Write json_view's dump() without a call or conversion per token (#5633)
* Write json_view's dump() without a call or conversion per token The default dump() (no indentation, no ensure_ascii) gets its own writer that makes the same walk and produces the same output: - the write position stays in a local variable instead of a member, so the compiler keeps it in a register across stores through aliasing char pointers; - strings and number tokens are copied with fixed-size 32-byte moves wherever enough source bytes remain, instead of one memcpy call per token; - the innermost open container lives in local variables; a stack that starts as a local array of 32 entries holds the rest; - unedited documents are walked through the node array in order, and integer tokens are read from the source directly. On top of that, float tokens of at most 15 significant digits are written straight from their digits via zmij::to_shortest() and write_shortest(), without converting to a double and back: such decimals are farther apart than a double's rounding interval, so the token's digits are the double's shortest digits. Tokens of 16+ digits, or edited values, still go through decimal_to_float(). The view's own NEON write_decimal() is removed in favor of the shared writer, and the dump output now grows in 64 KiB steps instead of being resized to its estimate at once. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix MSVC C4127 and Infer finding in the view serializer Test the template options through enabled() so that conditions combined with them are not constant, and mark the nav::value() null dereference as a false positive like materialize.hpp does. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix out-of-bounds read when dumping a float token of an unchecked image A float node of an image loaded with image_check::bounds can hold a token whose bytes are not digits, so its value need not have the digit count recorded in the node. write_double_at() passed that count to write_short_decimal(), which indexes its table of powers of ten by it: an assertion failure in debug builds, an out-of-bounds read in release builds. Use the counted overload only if the value has exactly that many digits, otherwise count them. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Tidy the view serializer: doxygen block placement, enabled() for a constant Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Use a named document in the dump test: root() of a temporary does not compile Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Test write_short_decimal and powers_of_ten_16 against to_chars Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix clang-tidy 22 findings in unit-to_chars.cpp Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Write doubles from their bits only where double is IEEE 754 binary64 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Split the value and pointer edit tests off unit-json_view_edit.cpp The MinGW linker of the Windows clang jobs cannot link object files with more than 32767 sections ("relocation truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'"). unit-json_view_edit.cpp has 35409 sections since the fast dump instantiates more code for editable documents, so its test cases "views and values", "deeply nested values", "pointers below a null value", and "strings of other documents are checked" move into unit-json_view_edit_values.cpp. The helper exception_of_call() that both files use moves into json_view_test_helpers.hpp. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Move the dump tests of this branch to unit-json_view_dump.cpp The dump and comparison tests moved to their own file in the dump pull request; the tests of the fast dump() join them there. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate the amalgamated headers Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Address review: one powers-of-ten table, literal sizes from the type - dtoa_impl::powers_of_ten() (10^0 to 10^19, one entry per line) replaces powers_of_ten_16() and the table of int_pow10() - put_literal() takes the length of a string literal from its type - small_stack holds the heap vector by reference - the growth step of the output buffer is written in hex Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Explain why the growth of the dump's stack is static Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate the amalgamated headers Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
1 parent
4971200b54
commit
4155e2938f
12 files changed
+1743
-395
No files matched your search
@@ -1342,6 +1342,66 @@ inline char* write_shortest(char* first, const zmij::shortest_decimal d) noexcep
|
||||
return end + (three ? 5 : 4);
|
||||
}
|
||||
|
||||
/// the powers of ten up to 10^19 (all that fit in std::uint64_t)
|
||||
inline const std::array<std::uint64_t, 20>& powers_of_ten() noexcept
|
||||
{
|
||||
static const std::array<std::uint64_t, 20> powers =
|
||||
{
|
||||
{
|
||||
1u,
|
||||
10u,
|
||||
100u,
|
||||
1000u,
|
||||
10000u,
|
||||
100000u,
|
||||
1000000u,
|
||||
10000000u,
|
||||
100000000u,
|
||||
1000000000u,
|
||||
10000000000u,
|
||||
100000000000u,
|
||||
1000000000000u,
|
||||
10000000000000u,
|
||||
100000000000000u,
|
||||
1000000000000000u,
|
||||
10000000000000000u,
|
||||
100000000000000000u,
|
||||
1000000000000000000u,
|
||||
10000000000000000000u
|
||||
}
|
||||
};
|
||||
return powers;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief digits * 10^exp, as write_decimal() writes it, for the digits of a
|
||||
double that need no conversion (count digits, at most 15, the first not 0;
|
||||
trailing zeros allowed): extended to 16 digits and written by write_shortest()
|
||||
|
||||
@return a pointer past the text; up to 41 bytes at @a first are written
|
||||
(some beyond the returned end)
|
||||
*/
|
||||
JSON_HEDLEY_NON_NULL(1)
|
||||
JSON_HEDLEY_RETURNS_NON_NULL
|
||||
inline char* write_short_decimal(char* first, std::uint64_t digits, int count, int exp) noexcept
|
||||
{
|
||||
JSON_ASSERT(digits >= powers_of_ten()[static_cast<std::size_t>(count - 1)] && count <= 15);
|
||||
const int scale = 16 - count;
|
||||
return write_shortest(first, zmij::shortest_decimal{digits * powers_of_ten()[static_cast<std::size_t>(scale)], exp - scale - 1, 0, false});
|
||||
}
|
||||
|
||||
/// as write_short_decimal(), counting the digits (not 0, less than 10^15)
|
||||
JSON_HEDLEY_NON_NULL(1)
|
||||
JSON_HEDLEY_RETURNS_NON_NULL
|
||||
inline char* write_short_decimal(char* first, std::uint64_t digits, int exp) noexcept
|
||||
{
|
||||
JSON_ASSERT(digits != 0 && digits < 1000000000000000u);
|
||||
// floor(log10(2^bits)) + 1 digits, or one less
|
||||
const int log2_bound = ((64 - count_leading_zeros(digits)) * 1233) >> 12;
|
||||
const int count = log2_bound + (digits >= powers_of_ten()[static_cast<std::size_t>(log2_bound)] ? 1 : 0);
|
||||
return write_short_decimal(first, digits, count, exp);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief whether FloatType is an IEEE 754 binary64 type (a double, or a long double
|
||||
that has the same format, as with MSVC and on Apple's Arm CPUs)
|
||||
|
||||
@@ -175,32 +175,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits(const unsigned char
|
||||
/// powers of ten up to 10^19 as integers
|
||||
inline std::uint64_t int_pow10(unsigned k) noexcept
|
||||
{
|
||||
static const std::array<std::uint64_t, 20> table =
|
||||
{
|
||||
{
|
||||
1u,
|
||||
10u,
|
||||
100u,
|
||||
1000u,
|
||||
10000u,
|
||||
100000u,
|
||||
1000000u,
|
||||
10000000u,
|
||||
100000000u,
|
||||
1000000000u,
|
||||
10000000000u,
|
||||
100000000000u,
|
||||
1000000000000u,
|
||||
10000000000000u,
|
||||
100000000000000u,
|
||||
1000000000000000u,
|
||||
10000000000000000u,
|
||||
100000000000000000u,
|
||||
1000000000000000000u,
|
||||
10000000000000000000u
|
||||
}
|
||||
};
|
||||
return table[k];
|
||||
return ::nlohmann::detail::dtoa_impl::powers_of_ten()[k];
|
||||
}
|
||||
|
||||
/// value of 0 < k < 8 digits at p in one step if [p, p + 8) lies below
|
||||
|
||||
@@ -31,7 +31,11 @@ namespace view
|
||||
{
|
||||
|
||||
/// append-only output buffer: writes through a raw pointer into a string that
|
||||
/// is resized ahead, and trimmed by finish()
|
||||
/// is resized ahead, and trimmed by finish(). The estimate is reserved, and the
|
||||
/// string grows in steps of 64 KiB within it: resize() fills the new bytes
|
||||
/// with zeros (before C++23, a string cannot grow without), and a small step
|
||||
/// is filled while the writer is about to use it, in the cache, instead of
|
||||
/// filling the whole estimate in memory first.
|
||||
template<typename StringType>
|
||||
class output_buffer
|
||||
{
|
||||
@@ -83,17 +87,49 @@ class output_buffer
|
||||
m_pos += n;
|
||||
}
|
||||
|
||||
/// the write position and the end of the writable space, for a writer
|
||||
/// that keeps the position in a local variable (set_cursor() hands it back)
|
||||
char* cursor() const noexcept
|
||||
{
|
||||
return m_pos;
|
||||
}
|
||||
|
||||
char* limit() const noexcept
|
||||
{
|
||||
return m_end;
|
||||
}
|
||||
|
||||
void set_cursor(char* p) noexcept
|
||||
{
|
||||
m_pos = p;
|
||||
}
|
||||
|
||||
private:
|
||||
/// the size of a growth step (a function: std::min() takes a reference,
|
||||
/// which a static constexpr member does not have before C++17)
|
||||
static constexpr std::size_t step() noexcept
|
||||
{
|
||||
return 0x10000;
|
||||
}
|
||||
|
||||
static StringType& sized(StringType& out, std::size_t estimate)
|
||||
{
|
||||
out.resize((std::max)(estimate, static_cast<std::size_t>(64)));
|
||||
out.reserve(estimate);
|
||||
out.resize((std::min)((std::max)(estimate, static_cast<std::size_t>(64)), step()));
|
||||
return out;
|
||||
}
|
||||
|
||||
NLOHMANN_VIEW_NOINLINE void grow(std::size_t n)
|
||||
{
|
||||
const auto used = static_cast<std::size_t>(m_pos - m_out.data());
|
||||
m_out.resize((std::max)(m_out.size() * 2, used + n + 256));
|
||||
// (a step does not go beyond the reserved estimate, so that a good
|
||||
// estimate is never copied to a larger allocation)
|
||||
const std::size_t size = (std::max)((std::min)(m_out.size() + step(), m_out.capacity()), used + n + 256);
|
||||
if (size > m_out.capacity())
|
||||
{
|
||||
m_out.reserve((std::max)(m_out.capacity() * 2, size));
|
||||
}
|
||||
m_out.resize(size);
|
||||
m_pos = &m_out[0] + used;
|
||||
m_end = &m_out[0] + m_out.size();
|
||||
}
|
||||
@@ -103,6 +139,117 @@ class output_buffer
|
||||
char* m_end;
|
||||
};
|
||||
|
||||
/// The length of the run at s that dump() writes unchanged without
|
||||
/// ensure_ascii: all bytes but quotes, backslashes, and control characters.
|
||||
/// Unlike detail::string_bulk_run(), non-ASCII bytes are not validated: the
|
||||
/// strings of a document are valid UTF-8 (a damaged image loaded with
|
||||
/// image_check::bounds can have others, which are then written unchanged).
|
||||
inline std::size_t plain_output_run(const unsigned char* s, std::size_t n) noexcept
|
||||
{
|
||||
constexpr std::uint64_t ones = 0x0101010101010101ull;
|
||||
constexpr std::uint64_t high = 0x8080808080808080ull;
|
||||
std::size_t i = 0;
|
||||
for (; i + 8 <= n; i += 8)
|
||||
{
|
||||
const std::uint64_t v = read_eight_bytes(s + i);
|
||||
const std::uint64_t q = v ^ 0x2222222222222222ull; // '"'
|
||||
const std::uint64_t b = v ^ 0x5C5C5C5C5C5C5C5Cull; // '\\'
|
||||
const std::uint64_t stop = (((q - ones) & ~q) | ((b - ones) & ~b) | ((v - 0x2020202020202020ull) & ~v)) & high;
|
||||
if (stop != 0)
|
||||
{
|
||||
// the lowest flagged byte is the first stop: borrows only flag bytes above a true one
|
||||
return i + (static_cast<std::size_t>(count_trailing_zeros(stop)) / 8);
|
||||
}
|
||||
}
|
||||
for (; i < n; ++i)
|
||||
{
|
||||
if (s[i] == '"' || s[i] == '\\' || s[i] < 0x20)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/// A stack that starts in a buffer of the caller (a local array) and moves to
|
||||
/// the heap (a vector of the caller) only when that is full, so that dumps of
|
||||
/// shallow documents need no allocation. The top is a pointer, as in
|
||||
/// std::vector. grow() is static and out of line: it gets the vector and the
|
||||
/// used range and returns the new storage, so the address of the stack never
|
||||
/// reaches a call. Otherwise the compiler would have to keep the three
|
||||
/// pointers in memory and load and store them on every push_back() and
|
||||
/// pop_back(), not just when growing.
|
||||
template<typename T>
|
||||
class small_stack
|
||||
{
|
||||
public:
|
||||
small_stack(T* buffer, std::size_t capacity, std::vector<T>& heap) noexcept
|
||||
: m_begin(buffer), m_top(buffer), m_end(buffer + capacity), m_heap(heap)
|
||||
{}
|
||||
small_stack(const small_stack&) = delete;
|
||||
small_stack(small_stack&&) = delete;
|
||||
small_stack& operator=(const small_stack&) = delete;
|
||||
small_stack& operator=(small_stack&&) = delete;
|
||||
~small_stack() = default;
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE void push_back(const T& x)
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(m_top == m_end))
|
||||
{
|
||||
const std::size_t used = size();
|
||||
const std::size_t capacity = 2 * static_cast<std::size_t>(m_end - m_begin);
|
||||
m_begin = grow(m_heap, m_begin, used, capacity);
|
||||
m_top = m_begin + used;
|
||||
m_end = m_begin + capacity;
|
||||
}
|
||||
*m_top++ = x;
|
||||
}
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE T& back() noexcept
|
||||
{
|
||||
return m_top[-1];
|
||||
}
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE void pop_back() noexcept
|
||||
{
|
||||
--m_top;
|
||||
}
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE bool empty() const noexcept
|
||||
{
|
||||
return m_top == m_begin;
|
||||
}
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE std::size_t size() const noexcept
|
||||
{
|
||||
return static_cast<std::size_t>(m_top - m_begin);
|
||||
}
|
||||
|
||||
private:
|
||||
/// the used entries moved to heap storage of the given capacity
|
||||
NLOHMANN_VIEW_NOINLINE static T* grow(std::vector<T>& heap, const T* begin, std::size_t used, std::size_t capacity)
|
||||
{
|
||||
std::vector<T> bigger(capacity);
|
||||
std::copy(begin, begin + used, bigger.begin());
|
||||
heap.swap(bigger);
|
||||
return heap.data();
|
||||
}
|
||||
|
||||
T* m_begin;
|
||||
T* m_top;
|
||||
T* m_end;
|
||||
std::vector<T>& m_heap;
|
||||
};
|
||||
|
||||
/// a string literal at w, without its terminating NUL (after room())
|
||||
/// @return a pointer past it
|
||||
template<std::size_t N>
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE char* put_literal(char* w, const char (&s)[N]) noexcept // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays)
|
||||
{
|
||||
std::memcpy(w, s, N - 1);
|
||||
return w + N - 1;
|
||||
}
|
||||
|
||||
/// how the view's dump() writes a value
|
||||
struct dump_style
|
||||
{
|
||||
@@ -129,6 +276,8 @@ class view_serializer
|
||||
using nav = navigation<Editable>;
|
||||
using string_t = typename BasicJsonType::string_t;
|
||||
using number_float_t = typename BasicJsonType::number_float_t;
|
||||
/// doubles that are IEEE 754 binary64 are written from their bits (Zmij)
|
||||
using fast_double = std::integral_constant < bool, std::is_same<number_float_t, double>::value && ::nlohmann::detail::dtoa_impl::is_binary64<double>::value >;
|
||||
|
||||
public:
|
||||
view_serializer(const document_data& d, string_t& out, std::size_t estimate, const dump_style& style)
|
||||
@@ -137,6 +286,18 @@ class view_serializer
|
||||
|
||||
void dump(const node* root)
|
||||
{
|
||||
if (!m_style.pretty && !m_style.ensure_ascii)
|
||||
{
|
||||
if (m_style.source_numbers)
|
||||
{
|
||||
dump_compact<true>(root);
|
||||
}
|
||||
else
|
||||
{
|
||||
dump_compact<false>(root);
|
||||
}
|
||||
return;
|
||||
}
|
||||
struct frame
|
||||
{
|
||||
const node* pos; ///< next element, or key of the next member
|
||||
@@ -144,7 +305,9 @@ class view_serializer
|
||||
bool object;
|
||||
bool first; ///< nothing written yet
|
||||
};
|
||||
std::vector<frame> stack;
|
||||
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::vector<frame> heap;
|
||||
small_stack<frame> stack(buffer.data(), buffer.size(), heap);
|
||||
const node* n = root;
|
||||
for (;;)
|
||||
{
|
||||
@@ -215,6 +378,293 @@ class view_serializer
|
||||
}
|
||||
|
||||
private:
|
||||
/// a string that is not a plain string of the source (decoded, or written
|
||||
/// by an edit), without ensure_ascii: runs without characters to escape
|
||||
/// are copied
|
||||
NLOHMANN_VIEW_NOINLINE void write_decoded(const node& n)
|
||||
{
|
||||
const auto* const s = reinterpret_cast<const unsigned char*>(m_doc.str(n)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
m_out.put('"');
|
||||
for (std::size_t i = 0; i < n.len;)
|
||||
{
|
||||
const std::size_t run = plain_output_run(s + i, n.len - i);
|
||||
if (run != 0)
|
||||
{
|
||||
m_out.put(reinterpret_cast<const char*>(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
write_codepoint<false>(s[i], s + i, 1); // a quote, a backslash, or a control character
|
||||
++i;
|
||||
}
|
||||
m_out.put('"');
|
||||
}
|
||||
|
||||
/// the copies of dump_compact() that are not fixed-size moves (long
|
||||
/// strings, or near the end of the source); out of line, so that the
|
||||
/// compiler does not merge the fixed-size moves into this call
|
||||
NLOHMANN_VIEW_NOINLINE static void copy_long(char* to, const char* from, std::size_t n) noexcept
|
||||
{
|
||||
std::memcpy(to, from, n);
|
||||
}
|
||||
|
||||
/// a compile-time option as a runtime condition: testing the template
|
||||
/// argument directly makes a condition like `Editable && c` constant when
|
||||
/// the option is off, which MSVC reports as C4127
|
||||
static NLOHMANN_VIEW_ALWAYS_INLINE bool enabled(bool option) noexcept
|
||||
{
|
||||
return option;
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief the compact output without ensure_ascii (the default dump())
|
||||
|
||||
The same walk as dump(), with the write position in a local variable
|
||||
(stores through char pointers would otherwise force a reload of the
|
||||
buffer's members after each one), and with strings and number tokens of
|
||||
the source copied by fixed-size moves of 32 bytes where the source has
|
||||
that many bytes left, instead of a library call per token. The buffer
|
||||
keeps 64 bytes of slack for the overshoot.
|
||||
*/
|
||||
template<bool SourceNumbers>
|
||||
void dump_compact(const node* root)
|
||||
{
|
||||
struct frame
|
||||
{
|
||||
const node* pos; ///< (editable documents) next element, or key of the next member
|
||||
const node* end;
|
||||
bool object;
|
||||
};
|
||||
std::array<frame, 32> buffer; // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init): written before read
|
||||
std::vector<frame> heap;
|
||||
small_stack<frame> stack(buffer.data(), buffer.size(), heap);
|
||||
const char* const src = m_doc.src;
|
||||
const char* const src_end = src + m_doc.size;
|
||||
char* w = m_out.cursor();
|
||||
char* lim = m_out.limit();
|
||||
// room for n bytes and the slack
|
||||
const auto room = [&](std::size_t n)
|
||||
{
|
||||
if (NLOHMANN_VIEW_UNLIKELY(static_cast<std::size_t>(lim - w) < n + 64))
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
m_out.reserve(n + 64);
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
}
|
||||
};
|
||||
// copy n bytes of the source (after room(n))
|
||||
const auto copy = [&](const char* from, std::size_t n)
|
||||
{
|
||||
if (n <= 32 && static_cast<std::size_t>(src_end - from) >= 32)
|
||||
{
|
||||
std::memcpy(w, from, 32);
|
||||
}
|
||||
else if (n <= 256 && static_cast<std::size_t>(src_end - from) >= n + 32)
|
||||
{
|
||||
for (std::size_t i = 0; i < n; i += 32)
|
||||
{
|
||||
std::memcpy(w + i, from + i, 32);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
copy_long(w, from, n);
|
||||
}
|
||||
w += n;
|
||||
};
|
||||
// a string that is not a plain string of the source (out of line, so
|
||||
// that the cursor stays in a register here)
|
||||
const auto escaped = [&](const node & n)
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
write_decoded(n);
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
};
|
||||
|
||||
// Read-only documents: the elements of a container follow it in the
|
||||
// node array, so the walk goes through the array in order, and a
|
||||
// frame only needs the end of its container. Editable documents: the
|
||||
// elements of a moved container live elsewhere, so a frame keeps the
|
||||
// position of the next element (see navigation).
|
||||
// The innermost open container is kept in registers (cur; end ==
|
||||
// nullptr: none), the stack holds the ones around it.
|
||||
frame cur{nullptr, nullptr, false};
|
||||
const node* n = root;
|
||||
for (;;)
|
||||
{
|
||||
// write the value at n (read-only documents: and advance n)
|
||||
bool opened = false;
|
||||
// false positive: n comes from nav::value(), which never returns null for a valid index
|
||||
// @infer-ignore NULLPTR_DEREFERENCE
|
||||
switch (static_cast<value_t>(n->kind))
|
||||
{
|
||||
case value_t::string:
|
||||
if ((n->flags & node_flags::storage) == 0)
|
||||
{
|
||||
room(n->len + 2);
|
||||
*w++ = '"';
|
||||
copy(src + n->off, n->len);
|
||||
*w++ = '"';
|
||||
}
|
||||
else
|
||||
{
|
||||
escaped(*n);
|
||||
}
|
||||
break;
|
||||
case value_t::number_integer:
|
||||
case value_t::number_unsigned:
|
||||
{
|
||||
const std::uint32_t len = number_length(*n);
|
||||
room(len);
|
||||
if (enabled(Editable) && (n->flags & node_flags::storage) != 0)
|
||||
{
|
||||
copy_long(w, m_doc.str(*n), len); // a canonical token written by an edit
|
||||
w += len;
|
||||
break;
|
||||
}
|
||||
const char* const token = src + n->off;
|
||||
if (!enabled(SourceNumbers) && NLOHMANN_VIEW_UNLIKELY(len == 2 && token[0] == '-' && token[1] == '0'))
|
||||
{
|
||||
*w++ = '0'; // parse() reads -0 as the integer 0
|
||||
}
|
||||
else
|
||||
{
|
||||
copy(token, len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case value_t::number_float:
|
||||
if (enabled(SourceNumbers) && (n->flags & node_flags::storage) != node_flags::edited)
|
||||
{
|
||||
room(n->len);
|
||||
copy(src + n->off, n->len);
|
||||
}
|
||||
else if (enabled(fast_double::value))
|
||||
{
|
||||
room(64);
|
||||
w = write_double_at(w, *n);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
write_float_node(*n);
|
||||
w = m_out.cursor();
|
||||
lim = m_out.limit();
|
||||
}
|
||||
break;
|
||||
case value_t::boolean:
|
||||
room(8);
|
||||
if ((n->flags & node_flags::is_true) != 0)
|
||||
{
|
||||
w = put_literal(w, "true");
|
||||
}
|
||||
else
|
||||
{
|
||||
w = put_literal(w, "false");
|
||||
}
|
||||
break;
|
||||
case value_t::object:
|
||||
case value_t::array:
|
||||
{
|
||||
const bool object = n->kind == static_cast<std::uint8_t>(value_t::object);
|
||||
room(8);
|
||||
if (n->len == 0)
|
||||
{
|
||||
w = object ? put_literal(w, "{}") : put_literal(w, "[]");
|
||||
}
|
||||
else
|
||||
{
|
||||
*w++ = object ? '{' : '[';
|
||||
stack.push_back(cur);
|
||||
if (Editable)
|
||||
{
|
||||
cur = frame{nav::first(m_doc, n), nav::end(m_doc, n), object};
|
||||
}
|
||||
else
|
||||
{
|
||||
cur = frame{nullptr, n + n->next, object};
|
||||
}
|
||||
opened = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case value_t::null:
|
||||
room(8);
|
||||
w = put_literal(w, "null");
|
||||
break;
|
||||
case value_t::binary: // LCOV_EXCL_LINE (not in a document)
|
||||
case value_t::discarded: // LCOV_EXCL_LINE
|
||||
default: // LCOV_EXCL_LINE
|
||||
break; // LCOV_EXCL_LINE
|
||||
}
|
||||
if (!Editable)
|
||||
{
|
||||
++n; // the next node: the first element of an opened container, or the node after a scalar
|
||||
}
|
||||
|
||||
// go to the next value: close finished containers, then separate
|
||||
// (a container just opened has an element)
|
||||
if (!opened)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
if (cur.end == nullptr)
|
||||
{
|
||||
m_out.set_cursor(w);
|
||||
m_out.finish();
|
||||
return;
|
||||
}
|
||||
if ((Editable ? cur.pos : n) != cur.end)
|
||||
{
|
||||
break;
|
||||
}
|
||||
room(1);
|
||||
*w++ = cur.object ? '}' : ']';
|
||||
cur = stack.back();
|
||||
stack.pop_back();
|
||||
}
|
||||
room(1);
|
||||
*w++ = ',';
|
||||
}
|
||||
const node* const at = Editable ? cur.pos : n;
|
||||
if (cur.object)
|
||||
{
|
||||
const node& key = *at;
|
||||
if ((key.flags & node_flags::storage) == 0)
|
||||
{
|
||||
room(key.len + 3);
|
||||
*w++ = '"';
|
||||
copy(src + key.off, key.len);
|
||||
w[0] = '"';
|
||||
w[1] = ':';
|
||||
w += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
escaped(key);
|
||||
room(1);
|
||||
*w++ = ':';
|
||||
}
|
||||
if (Editable)
|
||||
{
|
||||
n = nav::value(at + 1);
|
||||
cur.pos = document_data::after(at + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
++n;
|
||||
}
|
||||
}
|
||||
else if (Editable)
|
||||
{
|
||||
n = nav::value(at);
|
||||
cur.pos = document_data::after(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void newline(std::size_t level)
|
||||
{
|
||||
if (m_style.pretty)
|
||||
@@ -266,7 +716,7 @@ class view_serializer
|
||||
}
|
||||
else
|
||||
{
|
||||
write_float(float_value<number_float_t>(m_doc, n));
|
||||
write_float_node(n);
|
||||
}
|
||||
break;
|
||||
case value_t::object: // LCOV_EXCL_LINE (containers are written by dump())
|
||||
@@ -278,6 +728,88 @@ class view_serializer
|
||||
}
|
||||
}
|
||||
|
||||
/// a float node as dump() writes it
|
||||
void write_float_node(const node& n)
|
||||
{
|
||||
write_float_node(n, fast_double {});
|
||||
}
|
||||
|
||||
void write_float_node(const node& n, std::false_type /*other*/)
|
||||
{
|
||||
write_float(float_value<number_float_t>(m_doc, n));
|
||||
}
|
||||
|
||||
void write_float_node(const node& n, std::true_type /*double*/)
|
||||
{
|
||||
m_out.reserve(64);
|
||||
m_out.set_cursor(write_double_at(m_out.cursor(), n));
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief (doubles) the float at n as dump() writes it, at w (64 bytes of room)
|
||||
|
||||
A token of at most 15 significant digits is written from its digits,
|
||||
without a conversion: two decimals of at most 15 digits are farther
|
||||
apart than the rounding interval of a (normal) double (the argument
|
||||
behind DBL_DIG), so the token's digits are the shortest ones of its
|
||||
double, which the library's conversion writes (Zmij). Other tokens are
|
||||
converted from the digits already read.
|
||||
*/
|
||||
char* write_double_at(char* w, const node& n)
|
||||
{
|
||||
const unsigned int_digits = n.extra & 0xFFu;
|
||||
const unsigned frac_digits = n.extra >> 8u;
|
||||
if ((n.flags & node_flags::storage) != node_flags::edited && int_digits + frac_digits <= 19)
|
||||
{
|
||||
const auto* const first = reinterpret_cast<const unsigned char*>(m_doc.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
const float_significand d = layout_decimal(first, first + n.len, int_digits, frac_digits, reinterpret_cast<const unsigned char*>(m_doc.src + m_doc.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
|
||||
// (the exponent keeps the value far from subnormals and overflow)
|
||||
if (d.w != 0 && d.w < 1000000000000000u && d.exponent >= -290 && d.exponent <= 290)
|
||||
{
|
||||
*w = '-';
|
||||
w += d.negative ? 1 : 0;
|
||||
// (without leading zeros, all digits of the token count; the
|
||||
// check also keeps an image that was only checked for bounds,
|
||||
// whose token may not be made of digits, from the counted
|
||||
// overload)
|
||||
const auto& powers = ::nlohmann::detail::dtoa_impl::powers_of_ten();
|
||||
const unsigned count = int_digits + frac_digits;
|
||||
return count - 1u < 15u && d.w >= powers[count - 1u] && d.w < powers[count]
|
||||
? ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast<int>(count), static_cast<int>(d.exponent))
|
||||
: ::nlohmann::detail::dtoa_impl::write_short_decimal(w, d.w, static_cast<int>(d.exponent));
|
||||
}
|
||||
return write_double_value_at(w, decimal_to_float<double>(d)); // (without reading the token again)
|
||||
}
|
||||
return write_double_value_at(w, static_cast<double>(float_value<number_float_t>(m_doc, n)));
|
||||
}
|
||||
|
||||
/// n bytes of text at w
|
||||
static char* write_text_at(char* w, const char* text, std::size_t n) noexcept
|
||||
{
|
||||
std::memcpy(w, text, n);
|
||||
return w + n;
|
||||
}
|
||||
|
||||
/// a double as dump() writes it, at w (64 bytes of room)
|
||||
static char* write_double_value_at(char* w, double x)
|
||||
{
|
||||
// (from the bits: without the checks of to_chars())
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &x, sizeof(bits));
|
||||
if (NLOHMANN_VIEW_UNLIKELY((bits & 0x7FF0000000000000u) == 0x7FF0000000000000u))
|
||||
{
|
||||
return write_text_at(w, "null", 4);
|
||||
}
|
||||
*w = '-';
|
||||
w += bits >> 63u;
|
||||
bits &= ~(std::uint64_t{1} << 63u);
|
||||
if (bits == 0)
|
||||
{
|
||||
return write_text_at(w, "0.0", 3);
|
||||
}
|
||||
return ::nlohmann::detail::dtoa_impl::write_shortest(w, ::nlohmann::detail::zmij::to_shortest(bits));
|
||||
}
|
||||
|
||||
/// as serializer::dump_float()
|
||||
void write_float(number_float_t x)
|
||||
{
|
||||
|
||||
@@ -623,8 +623,10 @@ class basic_json_view
|
||||
style.indent_char = indent_char;
|
||||
style.ensure_ascii = ensure_ascii;
|
||||
style.source_numbers = numbers == number_format::source;
|
||||
// the compact text is about as long as the source text of the value
|
||||
const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 64;
|
||||
// the compact text is about as long as the source text of the value;
|
||||
// the compact writer keeps 64 bytes of slack, so that it does not grow
|
||||
// the buffer just before the end
|
||||
const std::size_t estimate = source_extent() + (style.pretty ? source_extent() / 2 : 0) + 160;
|
||||
detail::view::view_serializer<BasicJsonType, Editable>(*m_doc, out, estimate, style).dump(m_node);
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user