ox_mf2_parser 0.1.6

High Performance MessageFormat 2 parser core with CST, diagnostics, recovery, semantic lowering, and binary snapshot support.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
// @license MIT
// @author kazuya kawaguchi (a.k.a. kazupon)

//! Borrowed and owned snapshot views with allocation-free accessors.
//!
//! Once [`crate::snapshot::decode_snapshot`] succeeds, validated
//! `SectionIndex` metadata is paired with the snapshot byte slice. The
//! returned view does not materialise a recursive object graph;
//! traversal slices the buffer on demand.

use std::sync::Arc;

use crate::diagnostic::{DiagnosticCode, DiagnosticSeverity};
use crate::error::{OxMf2ErrorCode, SourceTextErrorCode};
use crate::snapshot::decoder::{diagnostic_code_from_u16_strict, syntax_kind_from_u16};
use crate::snapshot::format::{
    read_u16_le, read_u32_le, RootId, SectionKind, StringId, DIAGNOSTIC_LABEL_RECORD_SIZE,
    DIAGNOSTIC_RECORD_SIZE, EDGE_RECORD_SIZE, NODE_RECORD_SIZE, NONE_REF, ROOT_RECORD_SIZE,
    SOURCE_RECORD_SIZE, STRING_OFFSET_RECORD_SIZE, TOKEN_RECORD_SIZE, TRIVIA_RECORD_SIZE,
};
use crate::span::{NodeId, SourceId, Span, TokenId, TriviaId};
use crate::syntax_kind::SyntaxKind;

/// Error returned when a snapshot accessor cannot resolve source
/// bytes through [`SourceView::source_slice`].
///
/// `NotIncluded` means the snapshot was produced with
/// `SnapshotOptions.include_source_text = false`, so the writer
/// did not embed any source bytes. `SpanOutOfBounds` means the
/// span extends past the encoded text or lands inside a multibyte
/// UTF-8 scalar. The distinction matters because the former is
/// recoverable by the caller (use external source text) while the
/// latter signals a logic bug or stale span.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SourceTextUnavailable {
    NotIncluded,
    SpanOutOfBounds,
}

impl core::fmt::Display for SourceTextUnavailable {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(match self {
            Self::NotIncluded => {
                "snapshot was encoded without source text (include_source_text = false)"
            }
            Self::SpanOutOfBounds => {
                "span extends past the encoded source text or splits a UTF-8 scalar"
            }
        })
    }
}

impl std::error::Error for SourceTextUnavailable {}

impl SourceTextUnavailable {
    #[inline]
    pub const fn code(self) -> SourceTextErrorCode {
        match self {
            Self::NotIncluded => SourceTextErrorCode::SourceTextNotIncluded,
            Self::SpanOutOfBounds => SourceTextErrorCode::SourceTextSpanOutOfBounds,
        }
    }

    #[inline]
    pub const fn as_ox_mf2_error_code(self) -> OxMf2ErrorCode {
        self.code().as_ox_mf2_error_code()
    }
}

/// Section payload range. Validated by the decoder so accessors can
/// slice the underlying snapshot buffer without re-checking bounds.
#[derive(Debug, Clone, Copy)]
pub struct SectionSlice {
    pub offset: u32,
    pub byte_len: u32,
    pub count: u32,
}

/// Validated section metadata index. Built by
/// [`crate::snapshot::decode_snapshot`].
#[derive(Debug, Clone, Copy)]
pub struct SectionIndex {
    pub roots: SectionSlice,
    pub sources: SectionSlice,
    pub nodes: SectionSlice,
    pub edges: SectionSlice,
    pub tokens: SectionSlice,
    pub trivia: Option<SectionSlice>,
    pub diagnostics: Option<SectionSlice>,
    pub diagnostic_labels: Option<SectionSlice>,
    pub string_offsets: SectionSlice,
    pub string_data: SectionSlice,
    pub source_text_data: Option<SectionSlice>,
    pub extended_data: Option<SectionSlice>,
}

/// Read-only section metadata returned from
/// [`SnapshotView::section`] / [`SnapshotViewOwned::section`].
#[derive(Debug, Clone, Copy)]
pub struct SectionView {
    pub kind: SectionKind,
    pub offset: u32,
    pub byte_len: u32,
    pub count: u32,
    pub record_size: u16,
    pub alignment: u8,
}

/// Borrowed snapshot view. The byte buffer's lifetime is the caller's
/// responsibility.
#[derive(Debug, Clone, Copy)]
pub struct SnapshotView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
}

impl<'a> SnapshotView<'a> {
    pub(crate) fn from_validated(bytes: &'a [u8], sections: SectionIndex) -> Self {
        Self { bytes, sections }
    }

    /// Raw snapshot bytes.
    pub fn as_bytes(&self) -> &'a [u8] {
        self.bytes
    }

    /// Validated section metadata.
    pub fn sections(&self) -> &SectionIndex {
        &self.sections
    }

    /// Section view by kind. Returns `None` for sections that are
    /// optional and absent.
    pub fn section(&self, kind: SectionKind) -> Option<SectionView> {
        let slice = match kind {
            SectionKind::Roots => Some(self.sections.roots),
            SectionKind::Sources => Some(self.sections.sources),
            SectionKind::Nodes => Some(self.sections.nodes),
            SectionKind::Edges => Some(self.sections.edges),
            SectionKind::Tokens => Some(self.sections.tokens),
            SectionKind::Trivia => self.sections.trivia,
            SectionKind::Diagnostics => self.sections.diagnostics,
            SectionKind::DiagnosticLabels => self.sections.diagnostic_labels,
            SectionKind::StringOffsets => Some(self.sections.string_offsets),
            SectionKind::StringData => Some(self.sections.string_data),
            SectionKind::SourceTextData => self.sections.source_text_data,
            SectionKind::ExtendedData => self.sections.extended_data,
        }?;
        Some(SectionView {
            kind,
            offset: slice.offset,
            byte_len: slice.byte_len,
            count: slice.count,
            record_size: kind.record_size(),
            alignment: crate::snapshot::format::SECTION_ALIGNMENT as u8,
        })
    }

    /// Number of root records.
    pub fn root_count(&self) -> u32 {
        self.sections.roots.count
    }

    /// Borrowed root view by id. Returns `None` if `id` is out of
    /// range.
    pub fn root(&self, id: RootId) -> Option<RootView<'a>> {
        if id.raw() >= self.sections.roots.count {
            return None;
        }
        Some(RootView {
            bytes: self.bytes,
            sections: self.sections,
            id,
        })
    }

    /// Number of node records.
    pub fn node_count(&self) -> u32 {
        self.sections.nodes.count
    }

    /// Number of token records.
    pub fn token_count(&self) -> u32 {
        self.sections.tokens.count
    }

    /// Number of trivia records (zero when the trivia section is
    /// absent).
    pub fn trivia_count(&self) -> u32 {
        self.sections.trivia.map_or(0, |s| s.count)
    }

    /// Borrowed node view by id.
    pub fn node(&self, id: NodeId) -> Option<NodeView<'a>> {
        if id.is_none() || id.raw() >= self.sections.nodes.count {
            return None;
        }
        Some(self.node_unchecked(id))
    }

    /// Borrowed node view by id after the caller has already ensured
    /// that `id < nodes.count`.
    pub(crate) fn node_unchecked(&self, id: NodeId) -> NodeView<'a> {
        debug_assert!(!id.is_none());
        debug_assert!(id.raw() < self.sections.nodes.count);
        NodeView {
            bytes: self.bytes,
            sections: self.sections,
            id,
        }
    }

    /// Borrowed token view by id.
    pub fn token(&self, id: TokenId) -> Option<TokenView<'a>> {
        if id.is_none() || id.raw() >= self.sections.tokens.count {
            return None;
        }
        Some(TokenView {
            bytes: self.bytes,
            sections: self.sections,
            id,
        })
    }

    /// Borrowed trivia view by id.
    pub fn trivia(&self, id: TriviaId) -> Option<TriviaView<'a>> {
        let trivia = self.sections.trivia?;
        if id.is_none() || id.raw() >= trivia.count {
            return None;
        }
        Some(TriviaView {
            bytes: self.bytes,
            sections: self.sections,
            id,
        })
    }

    /// Number of source records.
    pub fn source_count(&self) -> u32 {
        self.sections.sources.count
    }

    /// Borrowed source view by id.
    pub fn source(&self, id: SourceId) -> Option<SourceView<'a>> {
        if id.is_none() || id.raw() >= self.sections.sources.count {
            return None;
        }
        Some(SourceView {
            bytes: self.bytes,
            sections: self.sections,
            id,
        })
    }

    /// String accessor. Returns `None` for the canonical sentinel
    /// `0xFFFF_FFFF` and for ids beyond the string offsets count.
    pub fn string(&self, id: StringId) -> Option<&'a str> {
        if id.is_none() {
            return None;
        }
        string_slice(self.bytes, &self.sections, id.raw())
    }

    /// Number of diagnostic records (zero when the section is absent).
    pub fn diagnostic_count(&self) -> u32 {
        self.sections.diagnostics.map_or(0, |s| s.count)
    }

    /// Borrowed diagnostic view by ordinal.
    pub fn diagnostic(&self, index: u32) -> Option<DiagnosticRecordView<'a>> {
        let diags = self.sections.diagnostics?;
        if index >= diags.count {
            return None;
        }
        Some(DiagnosticRecordView {
            bytes: self.bytes,
            sections: self.sections,
            index,
        })
    }
}

/// Owned snapshot view. Shares ownership of the buffer via [`Arc<[u8]>`]
/// so accessors can outlive the original byte producer.
#[derive(Debug, Clone)]
pub struct SnapshotViewOwned {
    bytes: Arc<[u8]>,
    sections: SectionIndex,
}

impl SnapshotViewOwned {
    pub(crate) fn from_validated(bytes: Arc<[u8]>, sections: SectionIndex) -> Self {
        Self { bytes, sections }
    }

    /// Raw snapshot bytes.
    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// Borrowed view over the same buffer.
    pub fn view(&self) -> SnapshotView<'_> {
        SnapshotView::from_validated(&self.bytes, self.sections)
    }

    /// Shared ownership of the underlying buffer.
    pub fn bytes_arc(&self) -> Arc<[u8]> {
        Arc::clone(&self.bytes)
    }

    /// See [`SnapshotView::section`].
    pub fn section(&self, kind: SectionKind) -> Option<SectionView> {
        self.view().section(kind)
    }

    /// See [`SnapshotView::sections`].
    pub fn sections(&self) -> SectionIndex {
        self.sections
    }
}

// ── Root / Source / Node / Token / Trivia views ────────────────────

#[derive(Debug, Clone, Copy)]
pub struct RootView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    id: RootId,
}

impl<'a> RootView<'a> {
    pub fn id(&self) -> RootId {
        self.id
    }

    pub fn root_node(&self) -> NodeId {
        let off = self.sections.roots.offset as usize + self.id.index() * ROOT_RECORD_SIZE as usize;
        NodeId::new(read_u32_le(self.bytes, off))
    }

    pub fn source_id(&self) -> SourceId {
        let off = self.sections.roots.offset as usize + self.id.index() * ROOT_RECORD_SIZE as usize;
        SourceId::new(read_u32_le(self.bytes, off + 4))
    }

    pub fn diagnostic_range(&self) -> (u32, u32) {
        let off = self.sections.roots.offset as usize + self.id.index() * ROOT_RECORD_SIZE as usize;
        (
            read_u32_le(self.bytes, off + 8),
            read_u32_le(self.bytes, off + 12),
        )
    }

    pub fn node(&self) -> NodeView<'a> {
        NodeView {
            bytes: self.bytes,
            sections: self.sections,
            id: self.root_node(),
        }
    }

    pub fn diagnostics(&self) -> DiagnosticIter<'a> {
        let (start, count) = self.diagnostic_range();
        DiagnosticIter {
            bytes: self.bytes,
            sections: self.sections,
            cursor: start,
            remaining: count,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct SourceView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    id: SourceId,
}

impl<'a> SourceView<'a> {
    pub fn id(&self) -> SourceId {
        self.id
    }

    fn base(&self) -> usize {
        self.sections.sources.offset as usize + self.id.index() * SOURCE_RECORD_SIZE as usize
    }

    pub fn path(&self) -> Option<&'a str> {
        let raw = read_u32_le(self.bytes, self.base() + 4);
        string_slice(self.bytes, &self.sections, raw)
    }

    pub fn locale(&self) -> Option<&'a str> {
        let raw = read_u32_le(self.bytes, self.base() + 8);
        string_slice(self.bytes, &self.sections, raw)
    }

    pub fn message_id(&self) -> Option<&'a str> {
        let raw = read_u32_le(self.bytes, self.base() + 12);
        string_slice(self.bytes, &self.sections, raw)
    }

    pub fn base_offset(&self) -> u32 {
        read_u32_le(self.bytes, self.base() + 16)
    }

    /// Snapshot-embedded source text bytes, if `include_source_text`
    /// was used during encoding.
    pub fn text(&self) -> Option<&'a str> {
        let base = self.base();
        let text_source = read_u32_le(self.bytes, base + 20);
        if text_source == NONE_REF {
            return None;
        }
        let offset = read_u32_le(self.bytes, base + 24);
        let len = read_u32_le(self.bytes, base + 28);
        let ed = self.sections.source_text_data?;
        let start = ed.offset as usize + offset as usize;
        let end = start + len as usize;
        core::str::from_utf8(&self.bytes[start..end]).ok()
    }

    /// Slice the snapshot-embedded source text by `span`, returning
    /// an explicit `SourceTextUnavailable` rather than `None` so
    /// callers can distinguish "source text not encoded" from
    /// "span out of bounds" — see `design/003` §"Source Section".
    ///
    /// `span` is interpreted as a zero-based byte offset into the
    /// snapshot-embedded text, matching the spans returned by
    /// [`NodeView::span`] / [`TokenView::span`] /
    /// [`TriviaView::span`] / [`DiagnosticRecordView::span`] /
    /// [`DiagnosticLabelView::span`]. `SourceView::base_offset` is
    /// metadata (the file offset where the embedded substring starts
    /// in the original source) and is intentionally NOT subtracted
    /// here. Absolute file positions are `base_offset + span_start`
    /// / `base_offset + span_end`; callers that already hold an
    /// absolute span must subtract `base_offset` themselves before
    /// calling `source_slice`.
    ///
    /// Returns `Err(SourceTextUnavailable::NotIncluded)` when the
    /// snapshot was produced with `include_source_text = false`
    /// (text source ref is the canonical `NONE_REF` sentinel).
    /// Returns `Err(SourceTextUnavailable::SpanOutOfBounds)` when
    /// `span` extends past the encoded text, when `span.start >
    /// span.end`, or when either endpoint lands inside a multibyte
    /// UTF-8 scalar.
    pub fn source_slice(&self, span: Span) -> Result<&'a str, SourceTextUnavailable> {
        let text = self.text().ok_or(SourceTextUnavailable::NotIncluded)?;
        slice_source_text(text, span)
    }

    /// Slice source text by `span`, falling back to caller-provided
    /// external text when the snapshot was encoded with
    /// `include_source_text = false`.
    ///
    /// The external text is explicit rather than looked up through a
    /// `SourceStore`: snapshot `SourceId`s are local to the emitted
    /// snapshot and do not have to match Phase 1 `SourceStore` ids.
    /// When snapshot-embedded text exists, it is preferred so this
    /// method has the same behaviour as [`Self::source_slice`].
    pub fn source_slice_with_external_text(
        &self,
        span: Span,
        external_source_text: &'a str,
    ) -> Result<&'a str, SourceTextUnavailable> {
        let text = self.text().unwrap_or(external_source_text);
        slice_source_text(text, span)
    }
}

fn slice_source_text(text: &str, span: Span) -> Result<&str, SourceTextUnavailable> {
    if span.start > span.end {
        return Err(SourceTextUnavailable::SpanOutOfBounds);
    }
    let len_u32 = text.len() as u32;
    if span.end > len_u32 {
        return Err(SourceTextUnavailable::SpanOutOfBounds);
    }
    let start = span.start as usize;
    let end = span.end as usize;
    text.get(start..end)
        .ok_or(SourceTextUnavailable::SpanOutOfBounds)
}

#[derive(Debug, Clone, Copy)]
pub struct NodeView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    id: NodeId,
}

impl<'a> NodeView<'a> {
    pub fn id(&self) -> NodeId {
        self.id
    }

    fn base(&self) -> usize {
        self.sections.nodes.offset as usize + self.id.index() * NODE_RECORD_SIZE as usize
    }

    pub fn kind(&self) -> SyntaxKind {
        syntax_kind_from_u16(read_u16_le(self.bytes, self.base())).unwrap_or(SyntaxKind::Unknown)
    }

    pub fn span(&self) -> Span {
        Span::new(
            read_u32_le(self.bytes, self.base() + 4),
            read_u32_le(self.bytes, self.base() + 8),
        )
    }

    pub fn child_count(&self) -> u32 {
        read_u32_le(self.bytes, self.base() + 16)
    }

    pub fn children(&self) -> ChildIter<'a> {
        let start = read_u32_le(self.bytes, self.base() + 12);
        let count = self.child_count();
        ChildIter {
            bytes: self.bytes,
            sections: self.sections,
            edge_start: start,
            remaining: count,
        }
    }

    pub fn child_at(&self, index: u32) -> Option<ChildView<'a>> {
        if index >= self.child_count() {
            return None;
        }
        let start = read_u32_le(self.bytes, self.base() + 12);
        Some(read_child(self.bytes, &self.sections, start + index))
    }
}

#[derive(Debug, Clone, Copy)]
pub enum ChildView<'a> {
    Node(NodeView<'a>),
    Token(TokenView<'a>),
}

#[derive(Debug, Clone)]
pub struct ChildIter<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    edge_start: u32,
    remaining: u32,
}

impl<'a> Iterator for ChildIter<'a> {
    type Item = ChildView<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.remaining == 0 {
            return None;
        }
        let child = read_child(self.bytes, &self.sections, self.edge_start);
        self.edge_start += 1;
        self.remaining -= 1;
        Some(child)
    }
}

fn read_child<'a>(bytes: &'a [u8], sections: &SectionIndex, edge_index: u32) -> ChildView<'a> {
    let off = sections.edges.offset as usize + edge_index as usize * EDGE_RECORD_SIZE as usize;
    let kind = read_u16_le(bytes, off);
    let ref_id = read_u32_le(bytes, off + 4);
    if kind == crate::snapshot::format::EDGE_KIND_TOKEN {
        ChildView::Token(TokenView {
            bytes,
            sections: *sections,
            id: TokenId::new(ref_id),
        })
    } else {
        ChildView::Node(NodeView {
            bytes,
            sections: *sections,
            id: NodeId::new(ref_id),
        })
    }
}

#[derive(Debug, Clone, Copy)]
pub struct TokenView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    id: TokenId,
}

impl<'a> TokenView<'a> {
    pub fn id(&self) -> TokenId {
        self.id
    }

    fn base(&self) -> usize {
        self.sections.tokens.offset as usize + self.id.index() * TOKEN_RECORD_SIZE as usize
    }

    pub fn kind(&self) -> SyntaxKind {
        syntax_kind_from_u16(read_u16_le(self.bytes, self.base())).unwrap_or(SyntaxKind::Unknown)
    }

    pub fn span(&self) -> Span {
        Span::new(
            read_u32_le(self.bytes, self.base() + 4),
            read_u32_le(self.bytes, self.base() + 8),
        )
    }

    pub fn source_id(&self) -> SourceId {
        SourceId::new(read_u32_le(self.bytes, self.base() + 12))
    }

    pub fn leading_trivia(&self) -> TriviaIter<'a> {
        let start = read_u32_le(self.bytes, self.base() + 16);
        let count = read_u32_le(self.bytes, self.base() + 20);
        TriviaIter {
            bytes: self.bytes,
            sections: self.sections,
            cursor: start,
            remaining: count,
        }
    }

    pub fn trailing_trivia(&self) -> TriviaIter<'a> {
        let start = read_u32_le(self.bytes, self.base() + 24);
        let count = read_u32_le(self.bytes, self.base() + 28);
        TriviaIter {
            bytes: self.bytes,
            sections: self.sections,
            cursor: start,
            remaining: count,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct TriviaView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    id: TriviaId,
}

impl TriviaView<'_> {
    pub fn id(&self) -> TriviaId {
        self.id
    }

    fn base(&self) -> usize {
        let trivia = self.sections.trivia.expect("trivia view requires section");
        trivia.offset as usize + self.id.index() * TRIVIA_RECORD_SIZE as usize
    }

    pub fn kind(&self) -> SyntaxKind {
        syntax_kind_from_u16(read_u16_le(self.bytes, self.base())).unwrap_or(SyntaxKind::Unknown)
    }

    pub fn span(&self) -> Span {
        Span::new(
            read_u32_le(self.bytes, self.base() + 4),
            read_u32_le(self.bytes, self.base() + 8),
        )
    }

    pub fn source_id(&self) -> SourceId {
        SourceId::new(read_u32_le(self.bytes, self.base() + 12))
    }
}

#[derive(Debug, Clone)]
pub struct TriviaIter<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    cursor: u32,
    remaining: u32,
}

impl<'a> Iterator for TriviaIter<'a> {
    type Item = TriviaView<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.remaining == 0 {
            return None;
        }
        let item = TriviaView {
            bytes: self.bytes,
            sections: self.sections,
            id: TriviaId::new(self.cursor),
        };
        self.cursor += 1;
        self.remaining -= 1;
        Some(item)
    }
}

#[derive(Debug, Clone, Copy)]
pub struct DiagnosticRecordView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    index: u32,
}

#[derive(Debug, Clone)]
pub struct DiagnosticIter<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    cursor: u32,
    remaining: u32,
}

impl<'a> Iterator for DiagnosticIter<'a> {
    type Item = DiagnosticRecordView<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.remaining == 0 {
            return None;
        }
        let item = DiagnosticRecordView {
            bytes: self.bytes,
            sections: self.sections,
            index: self.cursor,
        };
        self.cursor += 1;
        self.remaining -= 1;
        Some(item)
    }
}

impl<'a> DiagnosticRecordView<'a> {
    fn base(&self) -> usize {
        let diags = self
            .sections
            .diagnostics
            .expect("diagnostic view requires section");
        diags.offset as usize + self.index as usize * DIAGNOSTIC_RECORD_SIZE as usize
    }

    pub fn source_id(&self) -> SourceId {
        SourceId::new(read_u32_le(self.bytes, self.base()))
    }

    pub fn span(&self) -> Span {
        Span::new(
            read_u32_le(self.bytes, self.base() + 4),
            read_u32_le(self.bytes, self.base() + 8),
        )
    }

    pub fn severity(&self) -> DiagnosticSeverity {
        match self.bytes[self.base() + 12] {
            0 => DiagnosticSeverity::Error,
            1 => DiagnosticSeverity::Warning,
            2 => DiagnosticSeverity::Information,
            _ => DiagnosticSeverity::Hint,
        }
    }

    pub fn code(&self) -> DiagnosticCode {
        let raw = read_u16_le(self.bytes, self.base() + 14);
        diagnostic_code_from_u16_strict(raw).unwrap_or(DiagnosticCode::Unspecified)
    }

    pub fn message(&self) -> Option<&'a str> {
        let raw = read_u32_le(self.bytes, self.base() + 16);
        string_slice(self.bytes, &self.sections, raw)
    }

    pub fn label_range(&self) -> (u32, u32) {
        (
            read_u32_le(self.bytes, self.base() + 20),
            read_u32_le(self.bytes, self.base() + 24),
        )
    }

    pub fn labels(&self) -> DiagnosticLabelIter<'a> {
        let (start, count) = self.label_range();
        DiagnosticLabelIter {
            bytes: self.bytes,
            sections: self.sections,
            cursor: start,
            remaining: count,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub struct DiagnosticLabelView<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    index: u32,
}

impl<'a> DiagnosticLabelView<'a> {
    fn base(&self) -> usize {
        let labels = self
            .sections
            .diagnostic_labels
            .expect("label view requires section");
        labels.offset as usize + self.index as usize * DIAGNOSTIC_LABEL_RECORD_SIZE as usize
    }

    pub fn source_id(&self) -> SourceId {
        SourceId::new(read_u32_le(self.bytes, self.base()))
    }

    pub fn span(&self) -> Span {
        Span::new(
            read_u32_le(self.bytes, self.base() + 4),
            read_u32_le(self.bytes, self.base() + 8),
        )
    }

    pub fn message(&self) -> Option<&'a str> {
        let raw = read_u32_le(self.bytes, self.base() + 12);
        string_slice(self.bytes, &self.sections, raw)
    }
}

#[derive(Debug, Clone)]
pub struct DiagnosticLabelIter<'a> {
    bytes: &'a [u8],
    sections: SectionIndex,
    cursor: u32,
    remaining: u32,
}

impl<'a> Iterator for DiagnosticLabelIter<'a> {
    type Item = DiagnosticLabelView<'a>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.remaining == 0 {
            return None;
        }
        let item = DiagnosticLabelView {
            bytes: self.bytes,
            sections: self.sections,
            index: self.cursor,
        };
        self.cursor += 1;
        self.remaining -= 1;
        Some(item)
    }
}

fn string_slice<'a>(bytes: &'a [u8], sections: &SectionIndex, id_raw: u32) -> Option<&'a str> {
    if id_raw == NONE_REF || id_raw >= sections.string_offsets.count {
        return None;
    }
    let rec_off = sections.string_offsets.offset as usize
        + id_raw as usize * STRING_OFFSET_RECORD_SIZE as usize;
    let off = read_u32_le(bytes, rec_off);
    let len = read_u32_le(bytes, rec_off + 4);
    let start = sections.string_data.offset as usize + off as usize;
    let end = start + len as usize;
    core::str::from_utf8(&bytes[start..end]).ok()
}