Repository navigation
Conversation
|
Besides merging the lower CI fixes (see #5617 to #5623), commit 5484f2e reformats — posted by Claude Code on behalf of @nlohmann |
3e2b810 to
7c179b3
Compare
7c179b3 to
bd22b6e
Compare
d1e2563 to
48307dc
Compare
48307dc to
bc56ac6
Compare
dfa17bd to
d26cbef
Compare
d26cbef to
da1ca7f
Compare
f959e59 to
10757c1
Compare
10757c1 to
3807030
Compare
3807030 to
02748c5
Compare
| : The character used for indentation if `indent` is greater than `0`. The default is ` ` (space). | ||
|
|
||
| `ensure_ascii` (in) | ||
| : If `ensure_ascii` is `#!cpp true`, all non-ASCII characters in the output are escaped with `\uXXXX` sequences, and |
There was a problem hiding this comment.
Will this even do anything, since everything in a basic_json_view is UTF-8?
There was a problem hiding this comment.
Ah, it also escapes control characters that JSON doesn't allow.
There was a problem hiding this comment.
Right: the input is valid UTF-8 either way, but ensure_ascii still writes non-ASCII as \uXXXX, and control characters are escaped in both modes.
— posted by Claude Code on behalf of @nlohmann
| The walk over the subtree is iterative, so the nesting depth it can write is limited by available memory only, not by | ||
| the call stack -- as for [`materialize()`](materialize.md). | ||
|
|
||
| Strings are escaped by the same rules as [`BasicJsonType::dump`](../basic_json/dump.md). With |
There was a problem hiding this comment.
Why not just write the source strings which weren't decoded? Oh, because you don't have that info recorded yet.
There was a problem hiding this comment.
Correct. A string with escape sequences is stored decoded, so its source spelling is gone, and the output must match BasicJsonType::dump anyway. Strings without escapes are already copied verbatim.
— posted by Claude Code on behalf of @nlohmann
There was a problem hiding this comment.
That could be an option, just like for numbers. I think the primary reason for that option would be to save the encoding cost, or for strings that decoding replaced invalid characters (does it do that in this feature?), but it can't be an option unless/until that data is available anyway.
There was a problem hiding this comment.
Agreed. Decoding never replaces characters in the view: invalid UTF-8 is a parse error, as in basic_json. So such an option would only save the escaping work, and as you say, it would first need the source position of escaped strings to be recorded (#5864).
— posted by Claude Code on behalf of @nlohmann
| the call stack -- as for [`materialize()`](materialize.md). | ||
|
|
||
| Strings are escaped by the same rules as [`BasicJsonType::dump`](../basic_json/dump.md). With | ||
| `#!cpp numbers == number_format::shortest`, floats are written with the library's shortest round-trip conversion, |
There was a problem hiding this comment.
split up into a separate paragraph for each case for easier reading.
| if (f.pos == f.end) | ||
| { | ||
| const bool object = f.object; | ||
| stack.pop_back(); |
| const bool object = n->kind == static_cast<std::uint8_t>(value_t::object); | ||
| if (n->len == 0) | ||
| { | ||
| m_out.put(object ? "{}" : "[]", 2); |
There was a problem hiding this comment.
literals, determine size automatically (multiple times)
| } | ||
| else | ||
| { | ||
| if (!numbers && ta != tb) |
There was a problem hiding this comment.
swap order of tests and then inline the numbers test so it's not needed if they're the same.
| } | ||
| if (ta == value_t::string) | ||
| { | ||
| if (!(a.string() == b.string())) |
There was a problem hiding this comment.
Why is this written that way? Is the result of .string() not required to have operator!=()?
| } | ||
| stack.push_back(std::move(f)); | ||
| } | ||
| else if (!(a.scalar() == b.scalar())) // numbers (also of different types), null, boolean |
| { | ||
| a.members(f.members_a, ordered); | ||
| b.members(f.members_b, ordered); | ||
| if (f.members_a.size() != f.members_b.size()) |
There was a problem hiding this comment.
Potential optimization: a single function that generates one of the members, then walks through the other one, and returns false as soon as it can be shown that the second will have fewer members.
I assume that the faster method will be to generate the larger one, and if the smaller one is still smaller, it's done. If it's larger or the same, compute how many duplicates will make it fail, and then as soon as you find that many duplicates, return false.
There was a problem hiding this comment.
That would help only for objects with duplicate keys, where the deduplicated lists can differ in size; for objects without duplicates, both lists have to be built anyway. Since duplicates are rare, I'd rather not add the complexity, unless a benchmark shows otherwise.
— posted by Claude Code on behalf of @nlohmann
There was a problem hiding this comment.
Okay, I thought those were only being built to check for duplicates. I probably hadn't read far enough to see the usage yet.
| const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size(); | ||
| if (f.next == count) | ||
| { | ||
| stack.pop_back(); |
There was a problem hiding this comment.
Could get around this and reuse allocations by never shrinking stack, but tracking a current member, incrementing and decrementing, and growing the stack vector when needed.
That would mean that this:
frame f;
f.object = ta == value_t::object;
...
stack.push_back(std::move(f));
becomes this:
++currentFrame;
if (current frame >= stack.size())
{
stack.emplace_back();
}
frame& f = stack[currentFrame];
f.clear(); // clears all of the vectors, resets next to 0
...
and stack.pop_back(); becomes --currentFrame;.
There was a problem hiding this comment.
Same here: f isn't read after pop_back(), the continue binds it again. I added a comment in 7b66cdb. Reusing the frames (and their vectors' capacity) instead of pushing fresh ones is a good idea; I'll try it as a measured follow-up rather than in this PR.
— posted by Claude Code on behalf of @nlohmann
There was a problem hiding this comment.
I mention it here because you've replaced that pattern in the past with a local copy and modifying the in-vector version using .back().
There was a problem hiding this comment.
Good point. I'll keep that pattern in mind for the frame-reuse follow-up: reuse frames via .back() and an index instead of a reference that push_back/pop_back can invalidate.
— posted by Claude Code on behalf of @nlohmann
669967a to
109cab3
Compare
| NLOHMANN_VIEW_ALWAYS_INLINE void put(const char* s, std::size_t n) | ||
| { | ||
| reserve(n); | ||
| std::memcpy(m_pos, s, n); |
| NLOHMANN_VIEW_ALWAYS_INLINE void put(const char* s, std::size_t n) | ||
| { | ||
| reserve(n); | ||
| std::memcpy(m_pos, s, n); |
From gregmarr's review of #5624: - The output buffer takes string literals without a length. - write_float() names the IEEE single/double condition once, as the library's serializer does. - equal() checks the types first and compares with !=; the reference to the top frame is noted as dangling after pop_back(). - dump.md: one paragraph per case in the notes. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
109cab3 to
2692892
Compare
Add basic_json_view::dump() and the comparison operators, and read floats from the parser's digit layout instead of rescanning the token. dump(indent, indent_char, ensure_ascii, number_format) writes a value the way ordered_json::parse(text).dump() writes it for the same arguments: members in document order, all of them should a key occur more than once; strings escaped by the same rules, using the library's scanning kernels; floats written with the library's to_chars conversion, so the output equals basic_json's byte for byte; integers copied from the source, where they are already canonical, except -0, which parse() reads as 0. There is no error_handler argument, because the view only holds valid UTF-8. number_format::source copies numbers exactly as they appear in the source (e.g. "1.50", "1E2", "-0"), which basic_json cannot provide. operator<< takes the indentation from the stream width, as for basic_json. The writer walks iteratively, so nesting depth is limited by memory only. operator== and operator!= compare two views, or a view and a basic_json value in either order, by the rules basic_json's operator== uses: numbers compare by value across their types, objects compare by their members with duplicate keys resolved as parse() resolves them, member order matters only where the object type keeps one, and discarded views compare as discarded basic_json values do, including under JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON. Nothing is materialized except single scalars. While parsing, the view now records where the integer digits, the fraction digits, and the exponent of a float token are, so floats and doubles with at most 19 digits are read from that layout with the library's decimal_to_float() instead of rescanning the token. Both round correctly, so the values are those of parse(). get<double>(), materialize(), dump(), and the comparisons all use it. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The source extent of a value was read from the next node, falling back to the rest of the document when that node held a decoded string. dump() of a small value could thus allocate a buffer as large as the document. Skip a few such nodes, cap the fallback estimate, and shrink a buffer that is much larger than its output. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
root() of a temporary document is deleted: its views would dangle. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
finish() copied the whole output when the buffer was more than twice as large as the result (citm dump +11%). The tighter source_extent() estimate already keeps the buffer of a small value small. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
dump() writes every member and == resolves duplicate keys as parse() does, whereas lookups find the first member of a duplicate key. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
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.cpp reaches that limit as the stack
grows, so its "json_view dump" and "json_view comparison" test cases move
into unit-json_view_dump.cpp. The test generator and has_duplicate_keys()
that both files use move into json_view_test_helpers.hpp.
The new file mentions JSON_HAS_CPP_17, so it is built for C++17 like the file
it was split from.
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
ci_single_binaries runs IWYU with --error on every header. For json_view.hpp it suggested adding <array> (std::array is used), the headers that json.hpp already provides (abi_config, abi_macros, input_adapters, json_pointer, cpp_future, string_concat, value_t, json_fwd), and <version> for std::nullptr_t, and removing <cstddef>. - include <array> - keep <cstddef> (nullptr_t, size_t; IWYU attributes them to <version> and <cstring>) - tell IWYU not to suggest the headers that json.hpp provides: the amalgamated json_view.hpp only includes json.hpp, so including them here would duplicate their definitions Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The review of #5620 renamed document_data's tape/tape_size/tape_cap to nodes/node_count/node_capacity and made the decode arena a std::vector<char>; adapt the code of this change to it. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
json_view.hpp uses std::istream and std::ostream itself, but IWYU sees them through the exported json.hpp and asked to remove both includes, which fails ci_single_binaries (single_json_view). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
From gregmarr's review of #5624: - The output buffer takes string literals without a length. - write_float() names the IEEE single/double condition once, as the library's serializer does. - equal() checks the types first and compares with !=; the reference to the top frame is noted as dangling after pop_back(). - dump.md: one paragraph per case in the notes. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The same std::unique_ptr false positive (USE_AFTER_DELETE) also shows for two json_documents parsed in one lambda in unit-json_view_dump.cpp (ci_infer). It affects any test that holds several documents, so the exclusion now covers tests/src/unit-json_view*.cpp; ASan and Valgrind cover these tests. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
… clang-tidy The overload takes the literal by array reference so that its length is a template parameter, like scan_literal() and the C array overloads of to_json() and from_json(). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2692892 to
3d318ab
Compare
Adds
dump(),operator<<,operator==andoperator!=tobasic_json_view, and reads floats from the parser's digit layout; part of the zero-copy view stack for #5295, on top of #5622.Summary
basic_json_view::dump(indent, indent_char, ensure_ascii, number_format)writes a value the wayordered_json::parse(text).dump()writes it, for the same arguments:detail::to_chars), so the output equalsbasic_json's byte for byte;-0, whichparse()reads as 0.error_handlerargument: the view only holds valid UTF-8.number_format::source, numbers are copied exactly as they appear in the source, e.g.1.50,1E2,-0, or all digits of a long integer.basic_jsoncannot provide this.operator<<takes the indentation from the stream width, as forbasic_json.detail/view/serializer.hpp) writes through a raw pointer into a string. The walk is iterative, so the nesting depth is limited by memory only.operator==andoperator!=compare two views, or a view and abasic_jsonvalue in either order. They answer whether the valuesparse()would produce are equal bybasic_json'soperator==:1 == 1.0);parse()resolves them: the last value, at the position of the first key. (Lookups such asoperator[]still return the first member; this is documented in both places.)ordered_json) and not otherwise, exactly asbasic_jsoncompares;basic_jsonvalues do, followingJSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON.operator<.floatanddoubletokens with at most 19 digits, the value is now read from that layout instead of rescanning the token:decimal_to_float()from Speed up the lexer: own float parser, string scan, and \u table #5738: Clinger's fast path where both operands are exact, and Eisel-Lemire otherwise, which needs no fallback for up to 19 digits.parse(). Longer tokens, and other float types, keep the library's conversion of the whole token.get<double>(),materialize(),dump()and the comparisons all use it.dump,number_format,operator==,operator!=andoperator<<; the view article has a "Writing a view back" section; the pages ofbasic_json::dumpandbasic_json::operator==link to the view's counterparts. TheViewDumpbenchmark joinstests/benchmarks.Performance
Measured on Apple M1 Max (Apple clang -O2) and an x86-64 KVM VPS (GCC 13 / Clang 18 -O2, pinned to one core, about ±5-10% noise), µs, best of 3 interleaved rounds of 15 runs; files from nativejson-benchmark.
Traversing canada (every number converted), previous PR → this PR: Apple M1 2074 → 1501 (-28%), x86-64 GCC 4296 → 3505 (-18%), Clang 4331 → 3348 (-23%).
dump()at this PR (a faster writer comes later in the stack):The core library (
json::parse/dump) is unaffected: the code is view-internal (detail/view/serializer.hpp,detail/view/compare.hpp, additions todetail/view/number.hpp, small changes tomaterialize.hpp,value.hppandlookup.hpp, and new members ofbasic_json_view).Tests
The tests of
dump()and the comparisons are in a new file,tests/src/unit-json_view_dump.cpp(with helpers shared throughtests/src/json_view_test_helpers.hpp), so that no test object exceeds the 32,767 sections that the MinGW linker of the Windows clang job accepts.dump(): 2,000 generated documents compared withordered_json::dump()for indentations -1/0/2, space and tab, andensure_ascii; strings with every kind of escape; numbers (5,000 random doubles, andfloatasnumber_float_t); duplicate keys, 100,000 levels of nesting, streams and discarded views; the buffer of a small value stays small, and output that outgrows the estimate is handled.basic_json, forjsonand forordered_json; numbers, duplicate keys, member order, discarded values and 100,000 levels of nesting.float) join the bit-for-bit comparison withparse().Public API
No breaking changes. The view API is new in this stack, so nothing here changes a released interface. New members of
basic_json_view:dump,operator<<,operator==,operator!=andnumber_format. Reading floats from the digit layout changes no observable behavior, only how the value is computed.User-visible decisions in this PR:
dump()writes members in document order and writes every occurrence of a repeated key, so for ajson_viewit can differ frommaterialize().dump().operator==resolves duplicate keys asparse()does (last value), while lookups find the first member.operator<and noerror_handlerargument fordump().Written by Claude Code.
🤖 Generated with Claude Code