Coverage Report

Created: 2026-08-13 06:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/glaze/include/glaze/json/lazy.hpp
Line
Count
Source
1
// Glaze Library
2
// For the license information refer to glaze.hpp
3
4
#pragma once
5
6
#include <array>
7
#include <string_view>
8
#include <type_traits>
9
#include <utility>
10
11
#include "glaze/json/read.hpp"
12
#include "glaze/json/skip.hpp"
13
#include "glaze/json/write.hpp"
14
#include "glaze/tuplet/tuple.hpp" // GLZ_NO_UNIQUE_ADDRESS
15
#include "glaze/util/expected.hpp"
16
17
namespace glz
18
{
19
   // Forward declarations (needed for circular references)
20
   template <auto Opts>
21
   struct lazy_document;
22
   template <auto Opts>
23
   class lazy_iterator;
24
   template <auto Opts>
25
   struct indexed_lazy_view;
26
   template <auto Opts>
27
   class indexed_lazy_iterator;
28
29
   // ============================================================================
30
   // Truly Lazy JSON Parser - No upfront processing
31
   // ============================================================================
32
33
   namespace detail
34
   {
35
      // lazy_json_view::get<std::string> unescapes its raw span by re-reading it through the JSON
36
      // reader. That read is independent of the document's options except for UTF-8 validation,
37
      // which the caller can turn off and must not be silently reinstated here.
38
      struct lazy_string_opts : opts
39
      {
40
         bool validate_utf8 = true;
41
      };
42
43
      template <auto Opts>
44
      consteval lazy_string_opts lazy_string_opts_for()
45
      {
46
         lazy_string_opts o{};
47
         o.validate_utf8 = check_validate_utf8(Opts);
48
         return o;
49
      }
50
51
      // Skip string - returns position after closing quote
52
      template <auto Opts>
53
      GLZ_ALWAYS_INLINE const char* skip_string_fast(const char* p, const char* end) noexcept
54
      {
55
         const char* const start = p;
56
         ++p; // skip opening quote
57
58
         // Find closing quote using memchr (SIMD-optimized in libc)
59
         while (p < end) {
60
            const char* q = static_cast<const char*>(std::memchr(p, '"', static_cast<size_t>(end - p)));
61
            if (!q) [[unlikely]] {
62
               return end; // unclosed string
63
            }
64
65
            // Count preceding backslashes to check if escaped
66
            size_t backslashes = 0;
67
            const char* check = q - 1;
68
            while (check > start && *check == '\\') {
69
               ++backslashes;
70
               --check;
71
            }
72
73
            p = q + 1;
74
            if ((backslashes & 1) == 0) {
75
               return p; // even backslashes = real quote
76
            }
77
            // odd backslashes = escaped quote, continue searching
78
         }
79
         return p;
80
      }
81
82
      // The escalated scan below jumps to the next byte the depth loops actually act on, so its
83
      // character set must be exactly the bytes lazy_char_class does not classify as `other`.
84
      // The two live in different headers, and that equality IS the correctness argument for
85
      // escalating, so pin it here rather than leaving it to a comment.
86
      inline constexpr std::array<char, 5> lazy_structural_chars{'"', '[', ']', '{', '}'};
87
88
      template <size_t... I>
89
      GLZ_ALWAYS_INLINE const char* find_next_structural(const char* p, const char* end,
90
                                                         std::index_sequence<I...>) noexcept
91
0
      {
92
0
         return glz::find_first_of<lazy_structural_chars[I]...>(p, end);
93
0
      }
94
95
      // Scans to the next byte the depth loops act on. Expanded from lazy_structural_chars so the
96
      // scan and the assertion below cannot describe different sets.
97
      GLZ_ALWAYS_INLINE const char* find_next_structural(const char* p, const char* end) noexcept
98
0
      {
99
0
         return find_next_structural(p, end, std::make_index_sequence<lazy_structural_chars.size()>{});
100
0
      }
101
102
      static_assert(
103
         [] {
104
            for (size_t c = 0; c < 256; ++c) {
105
               const bool structural = lazy_char_class[c] != lazy_char_type::other;
106
               bool listed = false;
107
               for (const char l : lazy_structural_chars) {
108
                  if (uint8_t(l) == c) listed = true;
109
               }
110
               // `number` bytes are non-`other` but are consumed by the run walk, not the scan.
111
               if (lazy_char_class[c] == lazy_char_type::number) continue;
112
               if (structural != listed) return false;
113
            }
114
            return true;
115
         }(),
116
         "the wide-number escalation set must match lazy_char_class's structural bytes");
117
118
      // Skip a number inside a container, where the only bytes that matter are the structural
119
      // ones. Returns the position just past the numeric run.
120
      //
121
      // The default walk is a byte-at-a-time table lookup, which is what most JSON wants: typical
122
      // numbers are a few bytes long and a wide scan cannot amortize its setup over them. The
123
      // lazy_wide_number_skip option targets documents built from long numeric runs, where it
124
      // escalates to the SWAR scan once a run is still going after 8 bytes. Escalating is always
125
      // semantically safe - inside a container every non-structural byte is ignorable either way -
126
      // so the option changes only how fast the same position is reached.
127
      template <auto Opts>
128
      GLZ_ALWAYS_INLINE const char* skip_number_lazy(const char* p, const char* end) noexcept
129
      {
130
         ++p;
131
         if constexpr (Opts.null_terminated) {
132
            // A null-terminated buffer stops on its own sentinel, and end may be absent entirely
133
            // for a detached view, so the wide scan does not apply here.
134
            while (numeric_table[uint8_t(*p)]) ++p;
135
            return p;
136
         }
137
         else if constexpr (check_lazy_wide_number_skip(Opts)) {
138
            const char* const short_run_end = (end - p) > 8 ? p + 8 : end;
139
            while (p < short_run_end && numeric_table[uint8_t(*p)]) ++p;
140
            if (p == short_run_end && p < end) {
141
               return find_next_structural(p, end);
142
            }
143
            return p;
144
         }
145
         else {
146
            while (p < end && numeric_table[uint8_t(*p)]) ++p;
147
            return p;
148
         }
149
      }
150
151
      // Skip from current position to depth zero (end of enclosing container)
152
      // Used after partial scanning to find container end efficiently
153
      template <auto Opts>
154
      GLZ_ALWAYS_INLINE const char* skip_to_depth_zero(const char* p, const char* end, int depth) noexcept
155
      {
156
         using enum lazy_char_type;
157
         while (depth > 0) {
158
            if constexpr (Opts.null_terminated) {
159
               if (*p == '\0') break;
160
            }
161
            else {
162
               if (p >= end) break;
163
            }
164
            switch (lazy_char_class[uint8_t(*p)]) {
165
            case quote:
166
               p = skip_string_fast<Opts>(p, end);
167
               break;
168
            case open:
169
               ++depth;
170
               ++p;
171
               break;
172
            case close:
173
               --depth;
174
               ++p;
175
               break;
176
            case number:
177
               p = skip_number_lazy<Opts>(p, end);
178
               break;
179
            default:
180
               ++p;
181
               break;
182
            }
183
         }
184
         return p;
185
      }
186
187
      // skip_value stops a scalar on the byte that follows it, so a scalar occupying the tail of
188
      // a document reports unexpected_end: it runs out of buffer (bounded) or reaches the '\0'
189
      // sentinel (null-terminated) with nothing left to look at. Nothing is truncated at the
190
      // document edge, so that report is spurious and the value is complete -- the same
191
      // situation finalize_read_context settles for readers that land there.
192
      //
193
      // Containers and strings are deliberately excluded. Each carries its own terminator, so
194
      // reaching the edge without one is genuine truncation and must stay an error.
195
      [[nodiscard]] inline bool lazy_scalar_reaches_end(const char lead, const error_code ec, const char* it,
196
                                                        const char* end) noexcept
197
0
      {
198
0
         return ec == error_code::unexpected_end && it == end && lead != '{' && lead != '[' && lead != '"';
199
0
      }
200
201
      // Skip any JSON value - optimized container skip
202
      // Returns pointer after the value
203
      template <auto Opts>
204
      GLZ_ALWAYS_INLINE const char* skip_value_lazy(const char* p, const char* end) noexcept
205
      {
206
         using enum lazy_char_type;
207
         if constexpr (!Opts.null_terminated) {
208
            // Non-null-terminated buffers have no '\0' sentinel, so a caller can reach here
209
            // with p == end (e.g. a key whose value is missing). Guard the tag dereference.
210
            if (p >= end) return p;
211
         }
212
         switch (*p) {
213
         case '"':
214
            return skip_string_fast<Opts>(p, end);
215
         case 't':
216
            return p + 4 <= end ? p + 4 : end;
217
         case 'f':
218
            return p + 5 <= end ? p + 5 : end;
219
         case 'n':
220
            return p + 4 <= end ? p + 4 : end;
221
         case '[':
222
         case '{': {
223
            int depth = 1;
224
            ++p;
225
226
            while (depth > 0) {
227
               if constexpr (Opts.null_terminated) {
228
                  if (*p == '\0') break;
229
               }
230
               else {
231
                  if (p >= end) break;
232
               }
233
               switch (lazy_char_class[uint8_t(*p)]) {
234
               case quote:
235
                  p = skip_string_fast<Opts>(p, end);
236
                  break;
237
               case open:
238
                  ++depth;
239
                  ++p;
240
                  break;
241
               case close:
242
                  --depth;
243
                  ++p;
244
                  break;
245
               case number:
246
                  p = skip_number_lazy<Opts>(p, end);
247
                  break;
248
               default:
249
                  ++p;
250
                  break;
251
               }
252
            }
253
            return p;
254
         }
255
         default: {
256
            // Number, or an unrecognized byte.
257
            const char* const start = p;
258
            if constexpr (Opts.null_terminated) {
259
               while (numeric_table[uint8_t(*p)]) ++p;
260
            }
261
            else {
262
               while (p < end && numeric_table[uint8_t(*p)]) ++p;
263
            }
264
            // Guarantee forward progress. A byte that begins no valid value (not a string,
265
            // literal, container, or number - impossible in well-formed JSON) leaves p unmoved,
266
            // which would spin every caller that skips values in a loop (size(), index(),
267
            // operator[], iteration) forever on malformed input. Treat it as a one-byte scalar so
268
            // the scan always advances whenever p < end. Bounded by end because json_end_ is the
269
            // authoritative message end: a byte within it is safe to step over, while at or past
270
            // end we must not move (the caller's own p >= end guard then stops the loop). On
271
            // valid input p is always a real value start, so this branch never fires - no
272
            // behavior change.
273
            if (p == start && p < end) ++p;
274
            return p;
275
         }
276
         }
277
      }
278
279
      // ============================================================================
280
      // Streaming cursor storage (lazy_streaming_cursor option)
281
      // ============================================================================
282
      //
283
      // A single slot on lazy_document remembering the byte extent of the most recently
284
      // consumed value. lazy_iterator::operator++ jumps to the recorded end instead of
285
      // re-scanning, but only when the recorded extent starts exactly at the element the
286
      // iterator is sitting on. That pointer-identity check is what makes a stale slot
287
      // harmless: a byte offset in the buffer identifies exactly one value, so an extent
288
      // that starts where the iterator stands always describes the value it is about to
289
      // skip. Anything else falls back to a normal scan.
290
291
      // Disabled: an empty type, so lazy_document pays nothing under GLZ_NO_UNIQUE_ADDRESS.
292
      struct lazy_extent_disabled
293
      {
294
0
         static constexpr void clear() noexcept {}
295
0
         static constexpr void set(const char*, const char*, const char*) noexcept {}
296
0
         [[nodiscard]] static constexpr bool try_jump(const char*, const char*, const char*&) noexcept { return false; }
297
      };
298
299
      // Offsets rather than pointers so a copied/moved document rebases onto its own buffer.
300
      // end_off == 0 means "no extent recorded" - a real value can never end at offset 0.
301
      struct lazy_extent
302
      {
303
         size_t start_off{};
304
         size_t end_off{};
305
306
0
         void clear() noexcept { end_off = 0; }
307
308
         void set(const char* base, const char* start, const char* end) noexcept
309
0
         {
310
0
            if (!base || start < base || end < start) [[unlikely]] {
311
0
               clear();
312
0
               return;
313
0
            }
314
0
            start_off = size_t(start - base);
315
0
            end_off = size_t(end - base);
316
0
         }
317
318
         [[nodiscard]] bool try_jump(const char* base, const char* pos, const char*& out) noexcept
319
0
         {
320
0
            if (end_off == 0 || !base) {
321
0
               return false;
322
0
            }
323
0
            if (pos != base + start_off) {
324
0
               // The extent describes some other value; leave it in place, it may still match a
325
0
               // later advance (an inner iterator can record before an outer one asks).
326
0
               return false;
327
0
            }
328
0
            out = base + end_off;
329
0
            clear(); // consumed exactly once
330
0
            return true;
331
0
         }
332
      };
333
334
      template <auto Opts>
335
      using lazy_extent_for = std::conditional_t<check_lazy_streaming_cursor(Opts), lazy_extent, lazy_extent_disabled>;
336
   } // namespace detail
337
338
   // ============================================================================
339
   // lazy_json_view - Truly lazy view with on-demand scanning
340
   // ============================================================================
341
342
   /**
343
    * @brief A truly lazy view into JSON data.
344
    *
345
    * No upfront processing - all navigation uses SWAR-accelerated scanning.
346
    *
347
    * For objects, parse_pos_ tracks the current scan position to enable
348
    * efficient sequential key access (O(n) total instead of O(n²)).
349
    *
350
    * Memory layout (48 bytes on 64-bit, 24 bytes on 32-bit):
351
    * - doc_: pointer to document (8/4 bytes)
352
    * - data_: pointer to value start in JSON (8/4 bytes)
353
    * - parse_pos_: current scan position for progressive parsing (8/4 bytes)
354
    * - key_: stored key for iteration (16/8 bytes - string_view)
355
    * - error_: error code (4 bytes)
356
    * - padding: 4/0 bytes
357
    */
358
   template <auto Opts = opts{}>
359
   struct lazy_json_view
360
   {
361
     private:
362
      const lazy_document<Opts>* doc_{};
363
      const char* data_{};
364
      mutable const char* parse_pos_{}; // Current scan position (advances on key access)
365
      std::string_view key_{};
366
      error_code error_{error_code::none};
367
368
      lazy_json_view(error_code ec) noexcept : error_(ec) {}
369
370
     public:
371
      lazy_json_view() = default;
372
      lazy_json_view(const lazy_document<Opts>* doc, const char* data) noexcept : doc_(doc), data_(data) {}
373
374
      [[nodiscard]] static lazy_json_view make_error(error_code ec) noexcept { return lazy_json_view{ec}; }
375
376
      [[nodiscard]] bool has_error() const noexcept { return error_ != error_code::none; }
377
      [[nodiscard]] error_code error() const noexcept { return error_; }
378
379
      // Type checking - direct from JSON byte
380
      [[nodiscard]] bool is_null() const noexcept { return has_error() || !data_ || *data_ == 'n'; }
381
      [[nodiscard]] bool is_boolean() const noexcept
382
      {
383
         return !has_error() && data_ && (*data_ == 't' || *data_ == 'f');
384
      }
385
      [[nodiscard]] bool is_number() const noexcept
386
      {
387
         return !has_error() && data_ && (is_digit(uint8_t(*data_)) || *data_ == '-');
388
      }
389
      [[nodiscard]] bool is_string() const noexcept { return !has_error() && data_ && *data_ == '"'; }
390
      [[nodiscard]] bool is_array() const noexcept { return !has_error() && data_ && *data_ == '['; }
391
      [[nodiscard]] bool is_object() const noexcept { return !has_error() && data_ && *data_ == '{'; }
392
393
      explicit operator bool() const noexcept { return !has_error() && data_ && *data_ != 'n'; }
394
395
      [[nodiscard]] const char* data() const noexcept { return data_; }
396
      [[nodiscard]] const char* json_end() const noexcept;
397
398
      /// @brief Get the raw JSON bytes for this value
399
      /// @return string_view of the raw JSON, or empty if error
400
      /// @note Useful for passing to glz::read_json to deserialize into a struct
401
      /// @note Consider using read_into<T>() instead for better performance
402
      [[nodiscard]] std::string_view raw_json() const noexcept
403
      {
404
         if (has_error() || !data_) return {};
405
         const char* end = detail::skip_value_lazy<Opts>(data_, json_end());
406
         return {data_, static_cast<size_t>(end - data_)};
407
      }
408
409
      /// @brief Parse this value directly into a C++ type (single-pass, no double scanning)
410
      /// @tparam T The type to parse into
411
      /// @param value The object to populate
412
      /// @return Error context (check error_ctx.error for success/failure)
413
      /// @note This is more efficient than raw_json() + read_json() as it avoids scanning twice
414
      template <class T>
415
      [[nodiscard]] error_ctx read_into(T& value) const
416
      {
417
         if (has_error()) {
418
            return error_ctx{0, error_};
419
         }
420
         if (!data_) {
421
            return error_ctx{0, error_code::unexpected_end};
422
         }
423
         // Use parse<JSON>::op which naturally stops at value end
424
         // This avoids the need to pre-scan to find the value's extent
425
         context ctx{};
426
         auto it = data_;
427
         auto end = json_end();
428
         parse<JSON>::op<Opts>(value, ctx, it, end);
429
         // Capture completeness before finalize_read_context, which downgrades
430
         // partial_read_complete and a depth-zero end_reached to success. Those leave `it`
431
         // short of the value's true end, so `it` is only the real extent when the parse
432
         // reported nothing at all.
433
         const bool consumed_whole_value = ctx.error == error_code::none;
434
         finalize_read_context<Opts>(ctx);
435
         if (bool(ctx.error)) {
436
            return error_ctx{static_cast<size_t>(it - data_), ctx.error};
437
         }
438
         // Streaming cursor: the value spans [data_, it). Record it so a subsequent iterator
439
         // advance over this element can jump rather than re-scan. Under partial_read the
440
         // parse deliberately stops early, so nothing is recorded and iteration falls back to
441
         // scanning - correct, just not accelerated.
442
         if constexpr (check_lazy_streaming_cursor(Opts)) {
443
            // doc_ is null only for detached subviews; lazy_json() views always carry it.
444
            if (doc_ && consumed_whole_value) [[likely]] {
445
               record_consumed_extent(it);
446
            }
447
         }
448
         return {};
449
      }
450
451
      // Record [data_, it) as consumed, but only when we can actually tell that `it` is this
452
      // value's end.
453
      //
454
      // read_into hands the iterator to parse<JSON>::op, and where that leaves it is up to the
455
      // from<JSON, T> specialization. Glaze's own readers stop at the value's end, but that is a
456
      // convention rather than something checkable here: glz::text deliberately swallows the
457
      // rest of the buffer, and a custom reader may consume only a prefix. Trusting `it` blindly
458
      // is how the cursor turns a well-formed document into a short or mis-valued element
459
      // stream, with no error anywhere.
460
      //
461
      // So only containers are recorded, and only when the parse finished exactly on the
462
      // element's own closing bracket. Scalars are skipped by the cheap paths anyway (a numeric
463
      // table walk or a memchr for strings), so declining to record them costs almost nothing
464
      // and removes the whole class of doubt.
465
      void record_consumed_extent(const char* it) const noexcept
466
      {
467
         const char open = *data_;
468
         const char close = (open == '{') ? '}' : ((open == '[') ? ']' : '\0');
469
         if (close == '\0') {
470
            return; // scalar: not worth recording
471
         }
472
         if (it <= data_ || it[-1] != close) {
473
            return; // the reader stopped somewhere other than this container's end
474
         }
475
         doc_->consumed_.set(doc_->json_data(), data_, it);
476
      }
477
478
      template <class T>
479
      [[nodiscard]] expected<T, error_ctx> get() const;
480
481
      [[nodiscard]] lazy_json_view operator[](size_t index) const;
482
      [[nodiscard]] lazy_json_view operator[](std::string_view key) const;
483
      [[nodiscard]] bool contains(std::string_view key) const;
484
      [[nodiscard]] size_t size() const;
485
      [[nodiscard]] bool empty() const noexcept;
486
487
      // Key access for object iteration
488
      [[nodiscard]] std::string_view key() const noexcept { return key_; }
489
490
      [[nodiscard]] lazy_iterator<Opts> begin() const;
491
      [[nodiscard]] lazy_iterator<Opts> end() const;
492
493
      /// @brief Build an index for O(1) iteration and random access
494
      /// @return indexed_lazy_view with pre-computed element positions
495
      [[nodiscard]] indexed_lazy_view<Opts> index() const;
496
497
     private:
498
      friend struct lazy_document<Opts>;
499
      friend class lazy_iterator<Opts>;
500
      friend struct indexed_lazy_view<Opts>;
501
      friend class indexed_lazy_iterator<Opts>;
502
503
      // Constructor with key for iteration
504
      lazy_json_view(const lazy_document<Opts>* doc, const char* data, std::string_view key) noexcept
505
         : doc_(doc), data_(data), key_(key)
506
      {}
507
508
      // Skip whitespace helper
509
      // Note: The minified option is not used here because branch prediction
510
      // makes the whitespace loop essentially free when there's no whitespace.
511
      // Benchmarks show no benefit from skipping this check.
512
      static void skip_ws(const char*& p, [[maybe_unused]] const char* end) noexcept
513
      {
514
         if constexpr (Opts.null_terminated) {
515
            while (whitespace_table[uint8_t(*p)]) ++p;
516
         }
517
         else {
518
            while (p < end && whitespace_table[uint8_t(*p)]) ++p;
519
         }
520
      }
521
522
      // Parse a key from current position, return key and advance p
523
      static std::string_view parse_key(const char*& p, const char* end) noexcept;
524
   };
525
526
   // ============================================================================
527
   // lazy_document - Minimal container, no upfront processing
528
   // ============================================================================
529
530
   template <auto Opts = opts{}>
531
   struct lazy_document
532
   {
533
     private:
534
      const char* json_{};
535
      size_t len_{};
536
      const char* root_data_{};
537
      mutable lazy_json_view<Opts> root_view_{}; // Cached root view with parse_pos_
538
539
      // Streaming cursor slot (lazy_streaming_cursor option). Empty, and therefore free, when
540
      // the option is off. Mutable because recording an extent is a caching side effect of
541
      // otherwise const traversal.
542
      GLZ_NO_UNIQUE_ADDRESS mutable detail::lazy_extent_for<Opts> consumed_{};
543
544
      friend struct lazy_json_view<Opts>;
545
      friend class lazy_iterator<Opts>;
546
547
      // Factory method for lazy_json
548
      template <auto O, class Buffer>
549
      friend expected<lazy_document<O>, error_ctx> lazy_json(Buffer&&);
550
551
      // Helper to initialize root_view_ with correct doc_ pointer
552
      void init_root_view() noexcept { root_view_ = lazy_json_view<Opts>{this, root_data_}; }
553
554
     public:
555
      lazy_document() = default;
556
557
      // Copy constructor - fix doc_ pointer and preserve parse_pos_
558
      lazy_document(const lazy_document& other)
559
         : json_(other.json_), len_(other.len_), root_data_(other.root_data_), consumed_(other.consumed_)
560
      {
561
         init_root_view();
562
         root_view_.parse_pos_ = other.root_view_.parse_pos_;
563
      }
564
565
      lazy_document& operator=(const lazy_document& other)
566
      {
567
         if (this != &other) {
568
            json_ = other.json_;
569
            len_ = other.len_;
570
            root_data_ = other.root_data_;
571
            consumed_ = other.consumed_;
572
            init_root_view();
573
            root_view_.parse_pos_ = other.root_view_.parse_pos_;
574
         }
575
         return *this;
576
      }
577
578
      // Move constructor - fix doc_ pointer and transfer parse_pos_
579
      lazy_document(lazy_document&& other) noexcept
580
         : json_(other.json_), len_(other.len_), root_data_(other.root_data_), consumed_(other.consumed_)
581
      {
582
         init_root_view();
583
         root_view_.parse_pos_ = other.root_view_.parse_pos_;
584
      }
585
586
      lazy_document& operator=(lazy_document&& other) noexcept
587
      {
588
         if (this != &other) {
589
            json_ = other.json_;
590
            len_ = other.len_;
591
            root_data_ = other.root_data_;
592
            consumed_ = other.consumed_;
593
            init_root_view();
594
            root_view_.parse_pos_ = other.root_view_.parse_pos_;
595
         }
596
         return *this;
597
      }
598
599
      /// @brief Get the root view (cached, enables progressive key scanning)
600
      [[nodiscard]] lazy_json_view<Opts>& root() noexcept { return root_view_; }
601
602
      [[nodiscard]] const lazy_json_view<Opts>& root() const noexcept { return root_view_; }
603
604
      [[nodiscard]] lazy_json_view<Opts> operator[](std::string_view key) const { return root_view_[key]; }
605
      [[nodiscard]] lazy_json_view<Opts> operator[](size_t index) const { return root_view_[index]; }
606
607
      [[nodiscard]] bool is_null() const noexcept { return !root_data_ || *root_data_ == 'n'; }
608
      [[nodiscard]] bool is_array() const noexcept { return root_data_ && *root_data_ == '['; }
609
      [[nodiscard]] bool is_object() const noexcept { return root_data_ && *root_data_ == '{'; }
610
611
      explicit operator bool() const noexcept { return !is_null(); }
612
613
      [[nodiscard]] const char* json_data() const noexcept { return json_; }
614
      [[nodiscard]] size_t json_size() const noexcept { return len_; }
615
616
      /// @brief Reset parse position to beginning (for re-scanning from start)
617
      void reset_parse_pos() noexcept { root_view_.parse_pos_ = nullptr; }
618
   };
619
620
   // ============================================================================
621
   // lazy_iterator - Forward iterator with lazy scanning
622
   // ============================================================================
623
624
   template <auto Opts>
625
   class lazy_iterator
626
   {
627
     private:
628
      const lazy_document<Opts>* doc_{};
629
      const char* json_end_{};
630
      const char* container_start_{}; // '[' or '{' of the container being iterated
631
      char close_char_{};
632
      bool is_object_{};
633
      bool at_end_{true};
634
      lazy_json_view<Opts> current_view_{}; // Stored view for parse_pos_ optimization
635
636
      // Streaming cursor: on reaching the close, publish this container's own extent so an
637
      // outer iterator advancing over it can jump too. That is what makes the optimization
638
      // compose through nesting - an inner loop running to completion pays for the outer
639
      // advance. No-op when the option is off.
640
      void record_container_extent(const char* close_pos) noexcept
641
      {
642
         if constexpr (check_lazy_streaming_cursor(Opts)) {
643
            if (doc_ && container_start_) [[likely]] {
644
               // close_pos is the close character, except on a truncated buffer where it is
645
               // json_end_ and there is nothing past it to include.
646
               const char* const extent_end = (close_pos < json_end_) ? close_pos + 1 : close_pos;
647
               doc_->consumed_.set(doc_->json_data(), container_start_, extent_end);
648
            }
649
         }
650
      }
651
652
      // Scan past the element beginning at elem_start, using the parse_pos_ shortcut when keyed
653
      // access already walked part of it.
654
      [[nodiscard]] const char* scan_past_element(const char* elem_start) const noexcept
655
      {
656
         if (current_view_.is_object() && current_view_.parse_pos_ && current_view_.parse_pos_ > elem_start) {
657
            // Skip the last accessed value, then scan to the end of this object (depth 1 -> 0).
658
            const char* p = detail::skip_value_lazy<Opts>(current_view_.parse_pos_, json_end_);
659
            return detail::skip_to_depth_zero<Opts>(p, json_end_, 1);
660
         }
661
         return detail::skip_value_lazy<Opts>(elem_start, json_end_);
662
      }
663
664
     public:
665
      using iterator_category = std::forward_iterator_tag;
666
      using value_type = lazy_json_view<Opts>;
667
      using difference_type = std::ptrdiff_t;
668
      using pointer = void;
669
      using reference = lazy_json_view<Opts>&;
670
671
      lazy_iterator() = default;
672
673
      lazy_iterator(const lazy_document<Opts>* doc, const char* container_start, const char* end, bool is_object);
674
675
      // Return reference to stored view - allows parse_pos_ optimization when user uses auto&
676
      reference operator*() { return current_view_; }
677
      const lazy_json_view<Opts>& operator*() const { return current_view_; }
678
679
      lazy_iterator& operator++();
680
681
      lazy_iterator operator++(int)
682
      {
683
         auto tmp = *this;
684
         ++*this;
685
         return tmp;
686
      }
687
688
      bool operator==(const lazy_iterator& other) const { return at_end_ == other.at_end_; }
689
      bool operator!=(const lazy_iterator& other) const { return !(*this == other); }
690
691
     private:
692
      void advance_to_next_element(const char*& pos);
693
      void skip_ws(const char*& p) noexcept
694
      {
695
         if constexpr (Opts.null_terminated) {
696
            while (whitespace_table[uint8_t(*p)]) ++p;
697
         }
698
         else {
699
            while (p < json_end_ && whitespace_table[uint8_t(*p)]) ++p;
700
         }
701
      }
702
   };
703
704
   // ============================================================================
705
   // indexed_lazy_view - Lazy view with pre-built element index for O(1) access
706
   // ============================================================================
707
708
   /**
709
    * @brief A lazy view with a pre-built index for O(1) iteration and random access.
710
    *
711
    * Created by calling `index()` on a lazy_json_view. Scans the container once
712
    * to build an index of element positions, then provides:
713
    * - O(1) iteration advancement (vs O(element_size) for lazy_json_view)
714
    * - O(1) random access by index
715
    * - O(1) size query
716
    *
717
    * Elements returned are still lazy_json_view objects, so nested access remains lazy.
718
    *
719
    * Memory: Base view + 8 bytes per element (+ 16 bytes per element for objects with keys)
720
    */
721
   template <auto Opts>
722
   struct indexed_lazy_view
723
   {
724
     private:
725
      const lazy_document<Opts>* doc_{};
726
      const char* json_end_{};
727
      std::vector<const char*> value_starts_;
728
      std::vector<std::string_view> keys_; // Only populated for objects
729
      bool is_object_{};
730
731
      friend class indexed_lazy_iterator<Opts>;
732
733
     public:
734
      indexed_lazy_view() = default;
735
736
      /// @brief Number of elements - O(1)
737
      [[nodiscard]] size_t size() const noexcept { return value_starts_.size(); }
738
739
      /// @brief Check if empty - O(1)
740
      [[nodiscard]] bool empty() const noexcept { return value_starts_.empty(); }
741
742
      /// @brief Check if this is an indexed object
743
      [[nodiscard]] bool is_object() const noexcept { return is_object_; }
744
745
      /// @brief Check if this is an indexed array
746
      [[nodiscard]] bool is_array() const noexcept { return !is_object_; }
747
748
      /// @brief O(1) random access by index
749
      [[nodiscard]] lazy_json_view<Opts> operator[](size_t index) const
750
      {
751
         if (index >= value_starts_.size()) {
752
            return lazy_json_view<Opts>::make_error(error_code::exceeded_static_array_size);
753
         }
754
         std::string_view key = is_object_ ? keys_[index] : std::string_view{};
755
         return lazy_json_view<Opts>{doc_, value_starts_[index], key};
756
      }
757
758
      /// @brief O(n) key lookup for objects (linear search in index)
759
      [[nodiscard]] lazy_json_view<Opts> operator[](std::string_view key) const
760
      {
761
         if (!is_object_) {
762
            return lazy_json_view<Opts>::make_error(error_code::get_wrong_type);
763
         }
764
         for (size_t i = 0; i < keys_.size(); ++i) {
765
            if (keys_[i] == key) {
766
               return lazy_json_view<Opts>{doc_, value_starts_[i], keys_[i]};
767
            }
768
         }
769
         return lazy_json_view<Opts>::make_error(error_code::key_not_found);
770
      }
771
772
      /// @brief Check if object contains key - O(n) linear search
773
      [[nodiscard]] bool contains(std::string_view key) const
774
      {
775
         if (!is_object_) return false;
776
         for (const auto& k : keys_) {
777
            if (k == key) return true;
778
         }
779
         return false;
780
      }
781
782
      [[nodiscard]] indexed_lazy_iterator<Opts> begin() const;
783
      [[nodiscard]] indexed_lazy_iterator<Opts> end() const;
784
785
     private:
786
      friend struct lazy_json_view<Opts>;
787
788
      // Private constructor used by lazy_json_view::index()
789
      indexed_lazy_view(const lazy_document<Opts>* doc, const char* json_end, bool is_object)
790
         : doc_(doc), json_end_(json_end), is_object_(is_object)
791
      {}
792
793
      void reserve(size_t n)
794
      {
795
         value_starts_.reserve(n);
796
         if (is_object_) {
797
            keys_.reserve(n);
798
         }
799
      }
800
801
      void add_element(const char* value_start, std::string_view key = {})
802
      {
803
         value_starts_.push_back(value_start);
804
         if (is_object_) {
805
            keys_.push_back(key);
806
         }
807
      }
808
   };
809
810
   // ============================================================================
811
   // indexed_lazy_iterator - O(1) advancement using pre-built index
812
   // ============================================================================
813
814
   template <auto Opts>
815
   class indexed_lazy_iterator
816
   {
817
     private:
818
      const indexed_lazy_view<Opts>* parent_{};
819
      size_t index_{};
820
      mutable lazy_json_view<Opts> current_view_{}; // Cached for reference return
821
822
     public:
823
      using iterator_category = std::random_access_iterator_tag;
824
      using value_type = lazy_json_view<Opts>;
825
      using difference_type = std::ptrdiff_t;
826
      using pointer = void;
827
      using reference = lazy_json_view<Opts>&;
828
829
      indexed_lazy_iterator() = default;
830
      indexed_lazy_iterator(const indexed_lazy_view<Opts>* parent, size_t index) : parent_(parent), index_(index) {}
831
832
      reference operator*() const
833
      {
834
         std::string_view key = parent_->is_object_ ? parent_->keys_[index_] : std::string_view{};
835
         current_view_ = lazy_json_view<Opts>{parent_->doc_, parent_->value_starts_[index_], key};
836
         return current_view_;
837
      }
838
839
      lazy_json_view<Opts>* operator->() const
840
      {
841
         operator*(); // Update current_view_
842
         return &current_view_;
843
      }
844
845
      indexed_lazy_iterator& operator++()
846
      {
847
         ++index_;
848
         return *this;
849
      }
850
851
      indexed_lazy_iterator operator++(int)
852
      {
853
         auto tmp = *this;
854
         ++index_;
855
         return tmp;
856
      }
857
858
      indexed_lazy_iterator& operator--()
859
      {
860
         --index_;
861
         return *this;
862
      }
863
864
      indexed_lazy_iterator operator--(int)
865
      {
866
         auto tmp = *this;
867
         --index_;
868
         return tmp;
869
      }
870
871
      indexed_lazy_iterator& operator+=(difference_type n)
872
      {
873
         index_ = static_cast<size_t>(static_cast<difference_type>(index_) + n);
874
         return *this;
875
      }
876
877
      indexed_lazy_iterator& operator-=(difference_type n)
878
      {
879
         index_ = static_cast<size_t>(static_cast<difference_type>(index_) - n);
880
         return *this;
881
      }
882
883
      indexed_lazy_iterator operator+(difference_type n) const
884
      {
885
         return {parent_, static_cast<size_t>(static_cast<difference_type>(index_) + n)};
886
      }
887
888
      indexed_lazy_iterator operator-(difference_type n) const
889
      {
890
         return {parent_, static_cast<size_t>(static_cast<difference_type>(index_) - n)};
891
      }
892
893
      difference_type operator-(const indexed_lazy_iterator& other) const
894
      {
895
         return static_cast<difference_type>(index_) - static_cast<difference_type>(other.index_);
896
      }
897
898
      reference operator[](difference_type n) const
899
      {
900
         std::string_view key =
901
            parent_->is_object_ ? parent_->keys_[index_ + static_cast<size_t>(n)] : std::string_view{};
902
         current_view_ =
903
            lazy_json_view<Opts>{parent_->doc_, parent_->value_starts_[index_ + static_cast<size_t>(n)], key};
904
         return current_view_;
905
      }
906
907
      bool operator==(const indexed_lazy_iterator& other) const { return index_ == other.index_; }
908
      bool operator!=(const indexed_lazy_iterator& other) const { return index_ != other.index_; }
909
      bool operator<(const indexed_lazy_iterator& other) const { return index_ < other.index_; }
910
      bool operator<=(const indexed_lazy_iterator& other) const { return index_ <= other.index_; }
911
      bool operator>(const indexed_lazy_iterator& other) const { return index_ > other.index_; }
912
      bool operator>=(const indexed_lazy_iterator& other) const { return index_ >= other.index_; }
913
914
      friend indexed_lazy_iterator operator+(difference_type n, const indexed_lazy_iterator& it) { return it + n; }
915
   };
916
917
   // ============================================================================
918
   // Implementation: lazy_json_view methods (truly lazy - no structural index)
919
   // ============================================================================
920
921
   template <auto Opts>
922
   inline const char* lazy_json_view<Opts>::json_end() const noexcept
923
   {
924
      return doc_ ? doc_->json_ + doc_->len_ : nullptr;
925
   }
926
927
   template <auto Opts>
928
   inline std::string_view lazy_json_view<Opts>::parse_key(const char*& p, const char* end) noexcept
929
   {
930
      // A non-null-terminated buffer has no '\0' sentinel, so a scan loop can reach the next
931
      // key position with p == end (e.g. after a trailing comma). Guard the opening dereference.
932
      if constexpr (!Opts.null_terminated) {
933
         if (p >= end) return {};
934
      }
935
      if (*p != '"') return {};
936
      const char* const start = p;
937
      ++p; // skip opening quote
938
      const char* key_start = p;
939
940
      while (p < end) {
941
         const char* quote = static_cast<const char*>(std::memchr(p, '"', static_cast<size_t>(end - p)));
942
         if (!quote) [[unlikely]] {
943
            p = end;
944
            return std::string_view{key_start, static_cast<size_t>(end - key_start)};
945
         }
946
947
         // Count preceding backslashes to check if escaped
948
         size_t backslashes = 0;
949
         const char* check = quote - 1;
950
         while (check > start && *check == '\\') {
951
            ++backslashes;
952
            --check;
953
         }
954
955
         if ((backslashes & 1) == 0) {
956
            // Even backslashes = real quote
957
            std::string_view key{key_start, static_cast<size_t>(quote - key_start)};
958
            p = quote + 1; // skip closing quote
959
            return key;
960
         }
961
         // Odd backslashes = escaped quote, continue searching
962
         p = quote + 1;
963
      }
964
      return std::string_view{key_start, static_cast<size_t>(p - key_start)};
965
   }
966
967
   template <auto Opts>
968
   inline lazy_json_view<Opts> lazy_json_view<Opts>::operator[](size_t index) const
969
   {
970
      if (has_error()) return *this;
971
      if (!is_array()) return make_error(error_code::get_wrong_type);
972
973
      const char* end = json_end();
974
      const char* p = data_ + 1; // skip '['
975
      skip_ws(p, end);
976
977
      // Check for empty array. json_end_ bounds the message in both modes: null_terminated does
978
      // not guarantee the '\0' sits at json_end_, so fall back to the bound here too.
979
      if (p >= end || *p == ']') return make_error(error_code::exceeded_static_array_size);
980
981
      // Skip 'index' elements using lazy scanning
982
      for (size_t i = 0; i < index; ++i) {
983
         p = detail::skip_value_lazy<Opts>(p, end);
984
         skip_ws(p, end);
985
986
         if (p >= end || *p == ']') return make_error(error_code::exceeded_static_array_size);
987
988
         if (*p == ',') {
989
            ++p;
990
            skip_ws(p, end);
991
         }
992
      }
993
994
      // The requested index landed at or past end-of-input (a trailing comma with no following
995
      // element, or an unclosed array); a view positioned at end would read past the buffer, or,
996
      // when null_terminated, off the message into trailing bytes.
997
      if (p >= end) return make_error(error_code::exceeded_static_array_size);
998
      return {doc_, p};
999
   }
1000
1001
   template <auto Opts>
1002
   inline lazy_json_view<Opts> lazy_json_view<Opts>::operator[](std::string_view key) const
1003
   {
1004
      if (has_error()) return *this;
1005
      if (!is_object()) return make_error(error_code::get_wrong_type);
1006
1007
      const char* end = json_end();
1008
      const char* obj_start = data_ + 1; // skip '{'
1009
1010
      // Determine search start position
1011
      // parse_pos_ points to the VALUE of the last found key (lazy skip)
1012
      // We need to skip past it before continuing the search
1013
      const char* search_start = obj_start;
1014
      if (parse_pos_ && parse_pos_ > data_) {
1015
         // Lazily skip past the last found value now
1016
         const char* p = parse_pos_;
1017
         p = detail::skip_value_lazy<Opts>(p, end);
1018
         skip_ws(p, end);
1019
         if (p < end && *p == ',') {
1020
            ++p;
1021
            skip_ws(p, end);
1022
         }
1023
         search_start = p;
1024
      }
1025
1026
      const char* p = search_start;
1027
      skip_ws(p, end);
1028
1029
      // Forward pass: search from current position to end of object
1030
      while (true) {
1031
         // Check for end of object. json_end_ bounds the message in both modes (null_terminated
1032
         // does not guarantee the '\0' sits at json_end_); this is also the loop's backstop
1033
         // against an unclosed object spinning forever on a key parse that never advances.
1034
         if (p >= end || *p == '}') break;
1035
1036
         // Parse key
1037
         auto k = parse_key(p, end);
1038
         skip_ws(p, end);
1039
1040
         // Skip ':'
1041
         if (p < end && *p == ':') {
1042
            ++p;
1043
            skip_ws(p, end);
1044
         }
1045
1046
         // Check if key matches
1047
         if (k == key) {
1048
            // A matched key whose value begins at or past json_end_ is truncated; a view
1049
            // positioned at end would read past the buffer (or, when null_terminated, off the
1050
            // message into trailing bytes). Return the error before storing parse_pos_ so a
1051
            // truncated match leaves no state.
1052
            if (p >= end) return make_error(error_code::unexpected_end);
1053
            parse_pos_ = p; // Store value position (lazy - don't skip yet)
1054
            return {doc_, p};
1055
         }
1056
1057
         // Skip value using lazy scanning
1058
         p = detail::skip_value_lazy<Opts>(p, end);
1059
         skip_ws(p, end);
1060
1061
         // Skip comma
1062
         if (p < end && *p == ',') {
1063
            ++p;
1064
            skip_ws(p, end);
1065
         }
1066
      }
1067
1068
      // Wrap-around pass: search from beginning to where we started
1069
      if (search_start != obj_start) {
1070
         p = obj_start;
1071
         skip_ws(p, end);
1072
1073
         while (p < search_start) {
1074
            // Check for end of object (see forward pass).
1075
            if (p >= end || *p == '}') break;
1076
1077
            // Parse key
1078
            auto k = parse_key(p, end);
1079
            skip_ws(p, end);
1080
1081
            // Skip ':'
1082
            if (p < end && *p == ':') {
1083
               ++p;
1084
               skip_ws(p, end);
1085
            }
1086
1087
            // Check if key matches
1088
            if (k == key) {
1089
               // A matched key whose value begins at or past json_end_ is truncated; a view
1090
               // positioned at end would read past the buffer (or, when null_terminated, off the
1091
               // message into trailing bytes). Return the error before storing parse_pos_ so a
1092
               // truncated match leaves no state.
1093
               if (p >= end) return make_error(error_code::unexpected_end);
1094
               parse_pos_ = p; // Store value position (lazy - don't skip yet)
1095
               return {doc_, p};
1096
            }
1097
1098
            // Skip value using lazy scanning
1099
            p = detail::skip_value_lazy<Opts>(p, end);
1100
            skip_ws(p, end);
1101
1102
            // Skip comma
1103
            if (p < end && *p == ',') {
1104
               ++p;
1105
               skip_ws(p, end);
1106
            }
1107
         }
1108
      }
1109
1110
      return make_error(error_code::key_not_found);
1111
   }
1112
1113
   template <auto Opts>
1114
   inline bool lazy_json_view<Opts>::contains(std::string_view key) const
1115
   {
1116
      auto result = (*this)[key];
1117
      return !result.has_error();
1118
   }
1119
1120
   template <auto Opts>
1121
   inline size_t lazy_json_view<Opts>::size() const
1122
   {
1123
      if (has_error() || !data_) return 0;
1124
      if (!is_array() && !is_object()) return 0;
1125
1126
      const char* end = json_end();
1127
      const char* p = data_ + 1;
1128
      skip_ws(p, end);
1129
1130
      const char close_char = is_array() ? ']' : '}';
1131
1132
      // json_end_ bounds the message in both modes: null_terminated does not guarantee the '\0'
1133
      // sits at json_end_, so fall back to the bound here and throughout the loop below.
1134
      if (p >= end || *p == close_char) return 0;
1135
1136
      size_t count = 0;
1137
      const bool is_obj = is_object();
1138
1139
      while (true) {
1140
         // Stop before scanning a key/value at or past end. This keeps skip_string_fast and
1141
         // skip_value_lazy from dereferencing off the buffer, and is the loop's backstop against
1142
         // an unclosed container running off the end.
1143
         if (p >= end) return count;
1144
1145
         if (is_obj) {
1146
            // Skip key
1147
            p = detail::skip_string_fast<Opts>(p, end);
1148
            skip_ws(p, end);
1149
            if (p < end && *p == ':') {
1150
               ++p;
1151
               skip_ws(p, end);
1152
            }
1153
            // The value begins at or past end (truncated pair); don't count it, so size() agrees
1154
            // with index() and iteration.
1155
            if (p >= end) return count;
1156
         }
1157
1158
         // Skip value
1159
         p = detail::skip_value_lazy<Opts>(p, end);
1160
         ++count;
1161
         skip_ws(p, end);
1162
1163
         if (p >= end || *p == close_char) return count;
1164
1165
         if (*p == ',') {
1166
            ++p;
1167
            skip_ws(p, end);
1168
         }
1169
      }
1170
   }
1171
1172
   template <auto Opts>
1173
   inline bool lazy_json_view<Opts>::empty() const noexcept
1174
   {
1175
      if (has_error() || !data_) return true;
1176
      if (is_null()) return true;
1177
      if (!is_array() && !is_object()) return false;
1178
1179
      const char* end = json_end();
1180
      const char* p = data_ + 1;
1181
      skip_ws(p, end);
1182
1183
      const char close_char = is_array() ? ']' : '}';
1184
      // json_end_ bounds the message in both modes (null_terminated does not guarantee the '\0'
1185
      // sits at json_end_); a container with nothing before end is empty. This keeps empty()
1186
      // consistent with size()/index()/iteration reporting 0 on a bare, unclosed container.
1187
      return p >= end || *p == close_char;
1188
   }
1189
1190
   // ============================================================================
1191
   // lazy_iterator implementation (truly lazy)
1192
   // ============================================================================
1193
1194
   template <auto Opts>
1195
   inline lazy_iterator<Opts>::lazy_iterator(const lazy_document<Opts>* doc, const char* container_start,
1196
                                             const char* end, bool is_object)
1197
      : doc_(doc), json_end_(end), container_start_(container_start), is_object_(is_object), at_end_(false)
1198
   {
1199
      close_char_ = is_object ? '}' : ']';
1200
      const char* pos = container_start + 1; // skip '[' or '{'
1201
      skip_ws(pos);
1202
1203
      // Check for empty container
1204
      if constexpr (Opts.null_terminated) {
1205
         if (*pos == close_char_) {
1206
            at_end_ = true;
1207
            record_container_extent(pos);
1208
            return;
1209
         }
1210
      }
1211
      else {
1212
         if (pos >= json_end_ || *pos == close_char_) {
1213
            at_end_ = true;
1214
            record_container_extent(pos);
1215
            return;
1216
         }
1217
      }
1218
1219
      // Position at first element and initialize current_view_
1220
      advance_to_next_element(pos);
1221
   }
1222
1223
   template <auto Opts>
1224
   inline void lazy_iterator<Opts>::advance_to_next_element(const char*& pos)
1225
   {
1226
      std::string_view key{};
1227
      if (is_object_) {
1228
         // Parse key
1229
         key = lazy_json_view<Opts>::parse_key(pos, json_end_);
1230
         skip_ws(pos);
1231
1232
         // Skip ':'
1233
         if (pos < json_end_ && *pos == ':') {
1234
            ++pos;
1235
            skip_ws(pos);
1236
         }
1237
      }
1238
      // A value that begins at or past json_end_ is a truncated element; stop iterating instead
1239
      // of handing back a view whose data pointer sits at end (every view op reads *data_, an
1240
      // out-of-bounds read for a non-null-terminated buffer). This bound is unconditional on
1241
      // purpose: json_end_ is the authoritative end of the message, whereas null_terminated only
1242
      // promises a '\0' sentinel exists, not that it sits at json_end_ (it may be trailing buffer
1243
      // content further on), so '\0' cannot substitute here. It is also the iterator's only
1244
      // backstop against spinning forever on unclosed null-terminated input.
1245
      if (pos >= json_end_) {
1246
         at_end_ = true;
1247
         return;
1248
      }
1249
      // Store key in current_view_ (will be set properly after this call)
1250
      current_view_ = lazy_json_view<Opts>{doc_, pos, key};
1251
   }
1252
1253
   template <auto Opts>
1254
   inline lazy_iterator<Opts>& lazy_iterator<Opts>::operator++()
1255
   {
1256
      if (at_end_) return *this;
1257
1258
      const char* const elem_start = current_view_.data();
1259
      const char* pos = elem_start;
1260
1261
      // Streaming cursor: if this element was already consumed end-to-end (read_into, or a
1262
      // nested iterator that ran to its close) the recorded extent says where it finishes, so
1263
      // skip the scan entirely.
1264
      bool jumped = false;
1265
      if constexpr (check_lazy_streaming_cursor(Opts)) {
1266
         if (doc_) [[likely]] {
1267
            jumped = doc_->consumed_.try_jump(doc_->json_data(), pos, pos);
1268
         }
1269
      }
1270
      if (not jumped) {
1271
         pos = scan_past_element(elem_start);
1272
      }
1273
1274
      skip_ws(pos);
1275
1276
      // Check for end
1277
      if constexpr (Opts.null_terminated) {
1278
         if (*pos == close_char_) {
1279
            at_end_ = true;
1280
            record_container_extent(pos);
1281
            return *this;
1282
         }
1283
      }
1284
      else {
1285
         if (pos >= json_end_ || *pos == close_char_) {
1286
            at_end_ = true;
1287
            record_container_extent(pos);
1288
            return *this;
1289
         }
1290
      }
1291
1292
      // Skip comma
1293
      if (*pos == ',') {
1294
         ++pos;
1295
         skip_ws(pos);
1296
      }
1297
1298
      // Advance to next element (updates current_view_)
1299
      advance_to_next_element(pos);
1300
1301
      return *this;
1302
   }
1303
1304
   template <auto Opts>
1305
   inline lazy_iterator<Opts> lazy_json_view<Opts>::begin() const
1306
   {
1307
      if (has_error() || !data_) return end();
1308
      if (!is_array() && !is_object()) return end();
1309
      return lazy_iterator<Opts>{doc_, data_, json_end(), is_object()};
1310
   }
1311
1312
   template <auto Opts>
1313
   inline lazy_iterator<Opts> lazy_json_view<Opts>::end() const
1314
   {
1315
      return lazy_iterator<Opts>{};
1316
   }
1317
1318
   // ============================================================================
1319
   // indexed_lazy_view implementation
1320
   // ============================================================================
1321
1322
   template <auto Opts>
1323
   inline indexed_lazy_iterator<Opts> indexed_lazy_view<Opts>::begin() const
1324
   {
1325
      return indexed_lazy_iterator<Opts>{this, 0};
1326
   }
1327
1328
   template <auto Opts>
1329
   inline indexed_lazy_iterator<Opts> indexed_lazy_view<Opts>::end() const
1330
   {
1331
      return indexed_lazy_iterator<Opts>{this, value_starts_.size()};
1332
   }
1333
1334
   // ============================================================================
1335
   // lazy_json_view::index() implementation
1336
   // ============================================================================
1337
1338
   template <auto Opts>
1339
   inline indexed_lazy_view<Opts> lazy_json_view<Opts>::index() const
1340
   {
1341
      // Return empty indexed view for non-containers or errors
1342
      if (has_error() || !data_ || (!is_array() && !is_object())) {
1343
         return indexed_lazy_view<Opts>{};
1344
      }
1345
1346
      const char* end = json_end();
1347
      indexed_lazy_view<Opts> result{doc_, end, is_object()};
1348
1349
      const char* p = data_ + 1; // skip '[' or '{'
1350
      skip_ws(p, end);
1351
1352
      const char close_char = is_array() ? ']' : '}';
1353
      const bool is_obj = is_object();
1354
1355
      // Check for empty container. json_end_ bounds the message in both modes: null_terminated
1356
      // does not guarantee the '\0' sits at json_end_, so fall back to the bound throughout.
1357
      if (p >= end || *p == close_char) return result;
1358
1359
      // Scan and record all element positions
1360
      while (true) {
1361
         // Stop before parsing a key/value at or past end: this keeps the scans from reading off
1362
         // the buffer and is the loop's backstop against an unclosed container looping forever
1363
         // (repeatedly recording an element positioned at end).
1364
         if (p >= end) break;
1365
1366
         std::string_view key{};
1367
         if (is_obj) {
1368
            // Parse and record key
1369
            key = parse_key(p, end);
1370
            skip_ws(p, end);
1371
1372
            // Skip ':'
1373
            if (p < end && *p == ':') {
1374
               ++p;
1375
               skip_ws(p, end);
1376
            }
1377
         }
1378
1379
         // A truncated element whose value begins at or past json_end_ must not be recorded:
1380
         // an indexed view would later hand back a view positioned at end.
1381
         if (p >= end) break;
1382
1383
         // Record element start position
1384
         result.add_element(p, key);
1385
1386
         // Skip value
1387
         p = detail::skip_value_lazy<Opts>(p, end);
1388
         skip_ws(p, end);
1389
1390
         // Check for end of container
1391
         if (p >= end || *p == close_char) break;
1392
1393
         // Skip comma
1394
         if (*p == ',') {
1395
            ++p;
1396
            skip_ws(p, end);
1397
         }
1398
      }
1399
1400
      return result;
1401
   }
1402
1403
   // ============================================================================
1404
   // lazy_json_view::get<T>() implementation
1405
   // ============================================================================
1406
1407
   template <auto Opts>
1408
   template <class T>
1409
   [[nodiscard]] inline expected<T, error_ctx> lazy_json_view<Opts>::get() const
1410
   {
1411
      if (has_error()) {
1412
         return unexpected(error_ctx{0, error_});
1413
      }
1414
1415
      const char* end = json_end();
1416
1417
      if constexpr (std::is_same_v<T, bool>) {
1418
         if (!is_boolean()) {
1419
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1420
         }
1421
         return *data_ == 't';
1422
      }
1423
      else if constexpr (std::is_same_v<T, std::nullptr_t>) {
1424
         if (!is_null()) {
1425
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1426
         }
1427
         return nullptr;
1428
      }
1429
      else if constexpr (std::is_same_v<T, std::string>) {
1430
         if (!is_string()) {
1431
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1432
         }
1433
         auto it = data_;
1434
         context ctx{};
1435
         skip_value<JSON>::op<Opts>(ctx, it, end);
1436
         if (bool(ctx.error)) {
1437
            return unexpected(error_ctx{0, ctx.error});
1438
         }
1439
         std::string_view raw{data_, static_cast<size_t>(it - data_)};
1440
         // The read validates UTF-8 unless the document's options turned it off, so the returned
1441
         // string is well formed whenever validation is enabled.
1442
         static constexpr auto string_opts = detail::lazy_string_opts_for<Opts>();
1443
         std::string unescaped{};
1444
         context read_ctx{};
1445
         if (const error_ctx ec = glz::read<string_opts>(unescaped, raw, read_ctx)) {
1446
            return unexpected(ec);
1447
         }
1448
         return unescaped;
1449
      }
1450
      else if constexpr (std::is_same_v<T, std::string_view>) {
1451
         if (!is_string()) {
1452
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1453
         }
1454
         auto it = data_ + 1;
1455
         context ctx{};
1456
         skip_string_view(ctx, it, end);
1457
         if (bool(ctx.error)) {
1458
            return unexpected(error_ctx{0, ctx.error});
1459
         }
1460
         return std::string_view{data_ + 1, static_cast<size_t>(it - data_ - 1)};
1461
      }
1462
      else if constexpr (std::is_same_v<T, double>) {
1463
         if (!is_number()) {
1464
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1465
         }
1466
         double value{};
1467
         auto [ptr, ec] = glz::from_chars<false>(data_, end, value);
1468
         if (ec != std::errc()) {
1469
            return unexpected(error_ctx{0, error_code::parse_number_failure});
1470
         }
1471
         return value;
1472
      }
1473
      else if constexpr (std::is_same_v<T, float>) {
1474
         if (!is_number()) {
1475
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1476
         }
1477
         float value{};
1478
         auto [ptr, ec] = glz::from_chars<false>(data_, end, value);
1479
         if (ec != std::errc()) {
1480
            return unexpected(error_ctx{0, error_code::parse_number_failure});
1481
         }
1482
         return value;
1483
      }
1484
      else if constexpr (std::is_same_v<T, int64_t>) {
1485
         if (!is_number()) {
1486
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1487
         }
1488
         int64_t value{};
1489
         auto it = data_;
1490
         if (!glz::atoi(value, it, end)) {
1491
            return unexpected(error_ctx{0, error_code::parse_number_failure});
1492
         }
1493
         return value;
1494
      }
1495
      else if constexpr (std::is_same_v<T, uint64_t>) {
1496
         if (!is_number()) {
1497
            return unexpected(error_ctx{0, error_code::get_wrong_type});
1498
         }
1499
         uint64_t value{};
1500
         auto it = data_;
1501
         if (!glz::atoi(value, it, end)) {
1502
            return unexpected(error_ctx{0, error_code::parse_number_failure});
1503
         }
1504
         return value;
1505
      }
1506
      else if constexpr (std::is_same_v<T, int32_t>) {
1507
         auto result = get<int64_t>();
1508
         if (!result) return unexpected(result.error());
1509
         return static_cast<int32_t>(*result);
1510
      }
1511
      else if constexpr (std::is_same_v<T, uint32_t>) {
1512
         auto result = get<uint64_t>();
1513
         if (!result) return unexpected(result.error());
1514
         return static_cast<uint32_t>(*result);
1515
      }
1516
      else {
1517
         static_assert(false_v<T>, "Unsupported type for lazy_json_view::get<T>()");
1518
      }
1519
   }
1520
1521
   // ============================================================================
1522
   // JSON writer for lazy_json_view
1523
   // ============================================================================
1524
1525
   template <auto Opts>
1526
   struct to<JSON, lazy_json_view<Opts>>
1527
   {
1528
      template <auto WriteOpts, class B>
1529
      GLZ_ALWAYS_INLINE static void op(const lazy_json_view<Opts>& view, is_context auto&& ctx, B&& b, auto& ix)
1530
      {
1531
         if (view.has_error()) {
1532
            ctx.error = view.error();
1533
            return;
1534
         }
1535
         if (!view.data()) {
1536
            dump<false>("null", b, ix);
1537
            return;
1538
         }
1539
1540
         const char* const view_end = view.json_end();
1541
         auto it = view.data();
1542
         context parse_ctx{};
1543
         // Skip under the document's own options, not a default-constructed set. `opts{}` has
1544
         // null_terminated = true, so a view over a buffer without a '\0' sentinel would scan
1545
         // past json_end() looking for one and copy out-of-window bytes into the output.
1546
         skip_value<JSON>::op<Opts>(parse_ctx, it, view_end);
1547
         if (bool(parse_ctx.error)) [[unlikely]] {
1548
            if (!detail::lazy_scalar_reaches_end(*view.data(), parse_ctx.error, it, view_end)) {
1549
               ctx.error = parse_ctx.error;
1550
               return;
1551
            }
1552
            // Complete after all: trim the run of whitespace skip_value walked into so the
1553
            // written bytes match raw_json() exactly.
1554
            while (it > view.data() && whitespace_table[uint8_t(it[-1])]) {
1555
               --it;
1556
            }
1557
         }
1558
1559
         const size_t n = static_cast<size_t>(it - view.data());
1560
         if (n == 0) [[unlikely]] {
1561
            // No JSON value is zero bytes. skip_value consumes nothing when the view is not on a
1562
            // value at all, which navigation can produce on malformed input: looking up "a" in
1563
            // {"a":} leaves the view on the closing brace. Writing nothing while reporting
1564
            // success would splice invalid JSON into the caller's buffer, so refuse.
1565
            ctx.error = error_code::syntax_error;
1566
            return;
1567
         }
1568
1569
         if constexpr (resizable<B>) {
1570
            if (ix + n > b.size()) [[unlikely]] {
1571
               b.resize((std::max)(b.size() * 2, ix + n));
1572
            }
1573
         }
1574
         else {
1575
            if (ix + n > b.size()) [[unlikely]] {
1576
               ctx.error = error_code::buffer_overflow;
1577
               return;
1578
            }
1579
         }
1580
         std::memcpy(&b[ix], view.data(), n);
1581
         ix += n;
1582
      }
1583
   };
1584
1585
   // ============================================================================
1586
   // lazy_json - Main entry point (truly lazy - minimal upfront work)
1587
   // ============================================================================
1588
1589
   /**
1590
    * @brief Create a lazy JSON document - no upfront processing.
1591
    *
1592
    * - Just validates first byte and stores buffer reference - O(1)
1593
    * - All work happens on-demand when accessing fields
1594
    *
1595
    * @tparam Opts Options for parsing
1596
    * @param buffer The JSON text buffer (must remain valid for document lifetime)
1597
    * @return lazy_document on success, error_ctx on failure
1598
    */
1599
   template <auto Opts = opts{}, class Buffer>
1600
   [[nodiscard]] inline expected<lazy_document<Opts>, error_ctx> lazy_json(Buffer&& buffer)
1601
   {
1602
      lazy_document<Opts> doc;
1603
      doc.json_ = buffer.data();
1604
      doc.len_ = buffer.size();
1605
1606
      // Find root value (skip leading whitespace)
1607
      const char* p = buffer.data();
1608
      const char* end = buffer.data() + buffer.size();
1609
1610
      if constexpr (Opts.null_terminated) {
1611
         while (whitespace_table[uint8_t(*p)]) ++p;
1612
      }
1613
      else {
1614
         while (p < end && whitespace_table[uint8_t(*p)]) ++p;
1615
      }
1616
1617
      if (p >= end) {
1618
         return unexpected(error_ctx{0, error_code::unexpected_end});
1619
      }
1620
1621
      // Validate first character is valid JSON start
1622
      const char c = *p;
1623
      if (c != '{' && c != '[' && c != '"' && c != 't' && c != 'f' && c != 'n' && !is_digit(uint8_t(c)) && c != '-') {
1624
         return unexpected(error_ctx{0, error_code::syntax_error});
1625
      }
1626
1627
      doc.root_data_ = p;
1628
      doc.init_root_view(); // Initialize cached root view
1629
      return doc;
1630
   }
1631
1632
   // ============================================================================
1633
   // read_json overload for lazy_json_view (single-pass deserialization)
1634
   // ============================================================================
1635
1636
   /// @brief Read JSON from a lazy_json_view into a C++ type
1637
   /// @tparam T The type to parse into
1638
   /// @tparam Opts The lazy view options
1639
   /// @param value The object to populate
1640
   /// @param view The lazy view to read from
1641
   /// @return Error context if parsing failed
1642
   /// @note This provides the familiar glz::read_json API while using the efficient single-pass read_into internally
1643
   template <class T, auto Opts>
1644
   [[nodiscard]] inline error_ctx read_json(T& value, const lazy_json_view<Opts>& view)
1645
   {
1646
      return view.template read_into<T>(value);
1647
   }
1648
1649
   /// @brief Read JSON from a lazy_json_view rvalue into a C++ type
1650
   /// @note Handles temporaries like glz::read_json(value, doc["field"])
1651
   template <class T, auto Opts>
1652
   [[nodiscard]] inline error_ctx read_json(T& value, lazy_json_view<Opts>&& view)
1653
   {
1654
      return view.template read_into<T>(value);
1655
   }
1656
1657
} // namespace glz