Skip to main content

gruel_util/
span.rs

1//! Source span and location types.
2//!
3//! Provides the fundamental types for tracking source locations throughout
4//! the compilation pipeline.
5
6/// A file identifier used to track which source file a span belongs to.
7///
8/// File IDs are indices into a file table maintained by the compiler.
9/// `FileId(0)` is reserved as the default/unknown file.
10#[derive(
11    Debug, Clone, Copy, PartialEq, Eq, Default, Hash, serde::Serialize, serde::Deserialize,
12)]
13pub struct FileId(pub u32);
14
15impl FileId {
16    /// The default file ID, used for single-file compilation or when
17    /// the file is unknown.
18    pub const DEFAULT: FileId = FileId(0);
19
20    /// File ID reserved for the synthetic compiler prelude (ADR-0065).
21    /// Always parsed first; used so the prelude has a stable FileId distinct
22    /// from any user file.
23    pub const PRELUDE: FileId = FileId(0xFFFF_FFFE);
24
25    /// File ID reserved for synthetic built-in types (ADR-0073). Built-ins
26    /// like `String` are not in any user file; the visibility check uses
27    /// this sentinel as their "home module" so non-`pub` builtin fields and
28    /// methods are unreachable from user code without any special-case path.
29    pub const BUILTIN: FileId = FileId(0xFFFF_FFFD);
30
31    /// Create a new file ID from an index.
32    #[inline]
33    pub const fn new(id: u32) -> Self {
34        Self(id)
35    }
36
37    /// Get the raw index value.
38    #[inline]
39    pub const fn index(self) -> u32 {
40        self.0
41    }
42}
43
44/// A span representing a range in the source code.
45///
46/// Spans use byte offsets into the source string and include a file identifier
47/// for multi-file compilation. They are designed to be small (12 bytes) and
48/// cheap to copy.
49#[derive(
50    Debug, Clone, Copy, PartialEq, Eq, Default, Hash, serde::Serialize, serde::Deserialize,
51)]
52pub struct Span {
53    /// The file this span belongs to
54    pub file_id: FileId,
55    /// Start byte offset (inclusive)
56    pub start: u32,
57    /// End byte offset (exclusive)
58    pub end: u32,
59}
60
61impl Span {
62    /// Create a new span from start and end byte offsets.
63    ///
64    /// Uses the default file ID. For multi-file compilation, use `with_file`.
65    #[inline]
66    pub const fn new(start: u32, end: u32) -> Self {
67        Self {
68            file_id: FileId::DEFAULT,
69            start,
70            end,
71        }
72    }
73
74    /// Create a new span with a specific file ID.
75    #[inline]
76    pub const fn with_file(file_id: FileId, start: u32, end: u32) -> Self {
77        Self {
78            file_id,
79            start,
80            end,
81        }
82    }
83
84    /// Create an empty span at a single position.
85    ///
86    /// Uses the default file ID. For multi-file compilation, use `point_in_file`.
87    #[inline]
88    pub const fn point(pos: u32) -> Self {
89        Self {
90            file_id: FileId::DEFAULT,
91            start: pos,
92            end: pos,
93        }
94    }
95
96    /// Create an empty span at a single position in a specific file.
97    #[inline]
98    pub const fn point_in_file(file_id: FileId, pos: u32) -> Self {
99        Self {
100            file_id,
101            start: pos,
102            end: pos,
103        }
104    }
105
106    /// Create a span covering two spans (from start of first to end of second).
107    ///
108    /// Uses the file ID from span `a`. Both spans should be from the same file.
109    #[inline]
110    pub const fn cover(a: Span, b: Span) -> Self {
111        Self {
112            file_id: a.file_id,
113            start: if a.start < b.start { a.start } else { b.start },
114            end: if a.end > b.end { a.end } else { b.end },
115        }
116    }
117
118    /// Extend this span to a new end position, preserving the file ID.
119    ///
120    /// Creates a span from `self.start` to `end` with the same file ID.
121    #[inline]
122    pub const fn extend_to(&self, end: u32) -> Self {
123        Self {
124            file_id: self.file_id,
125            start: self.start,
126            end,
127        }
128    }
129
130    /// Get the start byte offset.
131    #[inline]
132    pub const fn start(&self) -> u32 {
133        self.start
134    }
135
136    /// The length of this span in bytes.
137    #[inline]
138    pub const fn len(&self) -> u32 {
139        self.end - self.start
140    }
141
142    /// Whether this span is empty.
143    #[inline]
144    pub const fn is_empty(&self) -> bool {
145        self.start == self.end
146    }
147
148    /// Returns `true` if `other` is entirely contained within this span.
149    ///
150    /// A span `a` contains span `b` if `a.start <= b.start` and `b.end <= a.end`.
151    /// An empty span at a boundary is considered contained.
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// use gruel_util::span::Span;
157    ///
158    /// let outer = Span::new(5, 20);
159    /// let inner = Span::new(10, 15);
160    /// let overlapping = Span::new(15, 25);
161    ///
162    /// assert!(outer.contains(inner));
163    /// assert!(!outer.contains(overlapping));
164    /// assert!(outer.contains(Span::point(10)));
165    /// ```
166    #[inline]
167    pub const fn contains(&self, other: Span) -> bool {
168        self.start <= other.start && other.end <= self.end
169    }
170
171    /// Returns `true` if this span contains the given byte position.
172    ///
173    /// The position is contained if `self.start <= pos < self.end`.
174    /// Note: the end position is exclusive, so `pos == self.end` returns `false`.
175    ///
176    /// # Example
177    ///
178    /// ```
179    /// use gruel_util::span::Span;
180    ///
181    /// let span = Span::new(5, 10);
182    /// assert!(!span.contains_pos(4));  // before span
183    /// assert!(span.contains_pos(5));   // at start (inclusive)
184    /// assert!(span.contains_pos(7));   // in middle
185    /// assert!(!span.contains_pos(10)); // at end (exclusive)
186    /// ```
187    #[inline]
188    pub const fn contains_pos(&self, pos: u32) -> bool {
189        self.start <= pos && pos < self.end
190    }
191
192    /// Convert to a Range<usize> for slicing.
193    #[inline]
194    pub const fn as_range(&self) -> std::ops::Range<usize> {
195        self.start as usize..self.end as usize
196    }
197
198    /// Compute the 1-based line number for this span's start position.
199    ///
200    /// Returns the line number (1-indexed) where this span begins.
201    ///
202    /// # Panics
203    ///
204    /// In debug builds, panics if `self.start` exceeds `source.len()`.
205    /// In release builds, out-of-bounds offsets are clamped to `source.len()`.
206    #[inline]
207    pub fn line_number(&self, source: &str) -> usize {
208        debug_assert!(
209            (self.start as usize) <= source.len(),
210            "span start {} exceeds source length {}",
211            self.start,
212            source.len()
213        );
214        byte_offset_to_line(source, self.start as usize)
215    }
216
217    /// Compute the 1-based line and column numbers for this span's start position.
218    ///
219    /// Returns `(line, column)` where both are 1-indexed. The column is the
220    /// number of bytes from the start of the line, plus 1.
221    ///
222    /// # Panics
223    ///
224    /// In debug builds, panics if `self.start` exceeds `source.len()`.
225    /// In release builds, out-of-bounds offsets are clamped to `source.len()`.
226    #[inline]
227    pub fn line_col(&self, source: &str) -> (usize, usize) {
228        debug_assert!(
229            (self.start as usize) <= source.len(),
230            "span start {} exceeds source length {}",
231            self.start,
232            source.len()
233        );
234        byte_offset_to_line_col(source, self.start as usize)
235    }
236}
237
238/// Convert a byte offset to a 1-based line number.
239///
240/// Counts the number of newlines before the given byte offset and adds 1.
241/// If `offset` exceeds `source.len()`, it is clamped to `source.len()`.
242///
243/// **Performance note**: This function is O(n) in the source length.
244/// For repeated lookups on the same source, use [`LineIndex`] for O(log n) lookups.
245#[inline]
246pub fn byte_offset_to_line(source: &str, offset: usize) -> usize {
247    source[..offset.min(source.len())]
248        .bytes()
249        .filter(|&b| b == b'\n')
250        .count()
251        + 1
252}
253
254/// Convert a byte offset to 1-based line and column numbers.
255///
256/// Returns `(line, column)` where both are 1-indexed.
257/// The column is the number of bytes from the start of the line, plus 1.
258/// If `offset` exceeds `source.len()`, it is clamped to `source.len()`.
259///
260/// **Performance note**: This function is O(n) in the source length.
261/// For repeated lookups on the same source, use [`LineIndex`] for O(log n) lookups.
262#[inline]
263pub fn byte_offset_to_line_col(source: &str, offset: usize) -> (usize, usize) {
264    let offset = offset.min(source.len());
265    let prefix = &source[..offset];
266
267    // Find the last newline before offset
268    match prefix.rfind('\n') {
269        Some(newline_pos) => {
270            let line = prefix.bytes().filter(|&b| b == b'\n').count() + 1;
271            let col = offset - newline_pos; // offset - newline_pos gives bytes after newline
272            (line, col)
273        }
274        None => {
275            // No newline before offset, so we're on line 1
276            (1, offset + 1)
277        }
278    }
279}
280
281/// Precomputed line offset index for efficient byte offset to line number conversion.
282///
283/// Building the index is O(n) in source length, but subsequent lookups are O(log n).
284/// Use this when you need to perform multiple line number lookups on the same source.
285///
286/// # Example
287///
288/// ```
289/// use gruel_util::span::LineIndex;
290///
291/// let source = "line1\nline2\nline3";
292/// let index = LineIndex::new(source);
293///
294/// assert_eq!(index.line_number(0), 1);  // Start of line 1
295/// assert_eq!(index.line_number(6), 2);  // Start of line 2
296/// assert_eq!(index.line_number(12), 3); // Start of line 3
297/// ```
298#[derive(Debug, Clone)]
299pub struct LineIndex {
300    /// Byte offsets where each line starts. line_starts[0] is always 0.
301    /// line_starts[i] is the byte offset of the start of line i+1 (1-indexed line number).
302    line_starts: Vec<u32>,
303    /// Total length of the source in bytes.
304    source_len: u32,
305}
306
307impl LineIndex {
308    /// Build a line index from source text.
309    ///
310    /// This scans the entire source once to find all newline positions.
311    /// Time complexity: O(n) where n is the source length.
312    pub fn new(source: &str) -> Self {
313        let mut line_starts = vec![0u32];
314        for (i, byte) in source.bytes().enumerate() {
315            if byte == b'\n' {
316                line_starts.push((i + 1) as u32);
317            }
318        }
319        Self {
320            line_starts,
321            source_len: source.len() as u32,
322        }
323    }
324
325    /// Get the 1-based line number for a byte offset.
326    ///
327    /// Time complexity: O(log n) where n is the number of lines.
328    ///
329    /// # Panics
330    ///
331    /// In debug builds, panics if `offset` exceeds the source length.
332    /// In release builds, out-of-bounds offsets are clamped to the source length.
333    #[inline]
334    pub fn line_number(&self, offset: u32) -> usize {
335        debug_assert!(
336            offset <= self.source_len,
337            "offset {} exceeds source length {}",
338            offset,
339            self.source_len
340        );
341        let offset = offset.min(self.source_len);
342
343        // Binary search for the line containing this offset.
344        // We want the largest line_start <= offset, which is partition_point - 1.
345
346        // partition_point returns the first index where the predicate is false,
347        // so line_idx - 1 is the line containing offset (but line_idx is already 1-indexed)
348        self.line_starts.partition_point(|&start| start <= offset)
349    }
350
351    /// Get the 1-based line number for a span's start position.
352    #[inline]
353    pub fn span_line_number(&self, span: Span) -> usize {
354        self.line_number(span.start)
355    }
356
357    /// Get the 1-based line and column numbers for a byte offset.
358    ///
359    /// Returns `(line, column)` where both are 1-indexed. The column is the
360    /// number of bytes from the start of the line, plus 1.
361    ///
362    /// Time complexity: O(log n) where n is the number of lines.
363    ///
364    /// # Panics
365    ///
366    /// In debug builds, panics if `offset` exceeds the source length.
367    /// In release builds, out-of-bounds offsets are clamped to the source length.
368    #[inline]
369    pub fn line_col(&self, offset: u32) -> (usize, usize) {
370        debug_assert!(
371            offset <= self.source_len,
372            "offset {} exceeds source length {}",
373            offset,
374            self.source_len
375        );
376        let offset = offset.min(self.source_len);
377
378        // Binary search for the line containing this offset.
379        let line_idx = self.line_starts.partition_point(|&start| start <= offset);
380        // line_idx is 1-indexed (partition_point returns first index where predicate is false)
381        let line_start = self.line_starts[line_idx - 1];
382        let col = (offset - line_start) as usize + 1;
383        (line_idx, col)
384    }
385
386    /// Get the 1-based line and column numbers for a span's start position.
387    ///
388    /// Returns `(line, column)` where both are 1-indexed.
389    #[inline]
390    pub fn span_line_col(&self, span: Span) -> (usize, usize) {
391        self.line_col(span.start)
392    }
393
394    /// Returns the number of lines in the source.
395    #[inline]
396    pub fn line_count(&self) -> usize {
397        self.line_starts.len()
398    }
399}
400
401impl From<std::ops::Range<usize>> for Span {
402    #[inline]
403    fn from(range: std::ops::Range<usize>) -> Self {
404        Self {
405            file_id: FileId::DEFAULT,
406            start: range.start as u32,
407            end: range.end as u32,
408        }
409    }
410}
411
412impl From<std::ops::Range<u32>> for Span {
413    #[inline]
414    fn from(range: std::ops::Range<u32>) -> Self {
415        Self {
416            file_id: FileId::DEFAULT,
417            start: range.start,
418            end: range.end,
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    #[test]
428    fn test_span_size() {
429        // Ensure Span stays small (12 bytes with file_id)
430        assert_eq!(std::mem::size_of::<Span>(), 12);
431    }
432
433    #[test]
434    fn test_file_id() {
435        assert_eq!(FileId::DEFAULT.index(), 0);
436        assert_eq!(FileId::new(42).index(), 42);
437    }
438
439    #[test]
440    fn test_span_with_file() {
441        let file = FileId::new(5);
442        let span = Span::with_file(file, 10, 20);
443        assert_eq!(span.file_id, file);
444        assert_eq!(span.start, 10);
445        assert_eq!(span.end, 20);
446    }
447
448    #[test]
449    fn test_span_point_in_file() {
450        let file = FileId::new(3);
451        let span = Span::point_in_file(file, 15);
452        assert_eq!(span.file_id, file);
453        assert_eq!(span.start, 15);
454        assert_eq!(span.end, 15);
455    }
456
457    #[test]
458    fn test_span_cover_preserves_file_id() {
459        let file = FileId::new(7);
460        let a = Span::with_file(file, 5, 10);
461        let b = Span::with_file(file, 15, 20);
462        let covered = Span::cover(a, b);
463        assert_eq!(covered.file_id, file);
464        assert_eq!(covered.start, 5);
465        assert_eq!(covered.end, 20);
466    }
467
468    #[test]
469    fn test_span_cover() {
470        let a = Span::new(5, 10);
471        let b = Span::new(15, 20);
472        let covered = Span::cover(a, b);
473        assert_eq!(covered, Span::new(5, 20));
474    }
475
476    #[test]
477    fn test_span_from_range() {
478        let span: Span = (5usize..10usize).into();
479        assert_eq!(span.start, 5);
480        assert_eq!(span.end, 10);
481    }
482
483    #[test]
484    fn test_byte_offset_to_line() {
485        let source = "line1\nline2\nline3";
486        // First line (offset 0-4)
487        assert_eq!(byte_offset_to_line(source, 0), 1);
488        assert_eq!(byte_offset_to_line(source, 4), 1);
489        // Second line (offset 6-10)
490        assert_eq!(byte_offset_to_line(source, 6), 2);
491        assert_eq!(byte_offset_to_line(source, 10), 2);
492        // Third line (offset 12+)
493        assert_eq!(byte_offset_to_line(source, 12), 3);
494        assert_eq!(byte_offset_to_line(source, 16), 3);
495    }
496
497    #[test]
498    fn test_span_line_number() {
499        let source = "let x = 1;\nlet y = 2;\nlet z = 3;";
500        // Span on line 1
501        let span1 = Span::new(0, 10);
502        assert_eq!(span1.line_number(source), 1);
503        // Span on line 2
504        let span2 = Span::new(11, 21);
505        assert_eq!(span2.line_number(source), 2);
506        // Span on line 3
507        let span3 = Span::new(22, 32);
508        assert_eq!(span3.line_number(source), 3);
509    }
510
511    #[test]
512    fn test_byte_offset_to_line_at_bounds() {
513        let source = "hello";
514        // At exactly the end of source
515        assert_eq!(byte_offset_to_line(source, 5), 1);
516        // Beyond source bounds - should clamp to source length
517        assert_eq!(byte_offset_to_line(source, 100), 1);
518    }
519
520    #[test]
521    fn test_byte_offset_to_line_empty_source() {
522        let source = "";
523        // Empty source should return line 1
524        assert_eq!(byte_offset_to_line(source, 0), 1);
525    }
526
527    #[test]
528    fn test_span_line_number_at_newline() {
529        let source = "a\nb";
530        // Span at the newline character itself
531        let span = Span::new(1, 2);
532        assert_eq!(span.line_number(source), 1);
533        // Span right after the newline
534        let span2 = Span::new(2, 3);
535        assert_eq!(span2.line_number(source), 2);
536    }
537
538    // ========================================================================
539    // LineIndex tests
540    // ========================================================================
541
542    #[test]
543    fn test_line_index_basic() {
544        let source = "line1\nline2\nline3";
545        let index = LineIndex::new(source);
546
547        // First line (offset 0-4)
548        assert_eq!(index.line_number(0), 1);
549        assert_eq!(index.line_number(4), 1);
550
551        // Second line (offset 6-10)
552        assert_eq!(index.line_number(6), 2);
553        assert_eq!(index.line_number(10), 2);
554
555        // Third line (offset 12+)
556        assert_eq!(index.line_number(12), 3);
557        assert_eq!(index.line_number(16), 3);
558    }
559
560    #[test]
561    fn test_line_index_matches_byte_offset_to_line() {
562        let source = "let x = 1;\nlet y = 2;\nlet z = 3;";
563        let index = LineIndex::new(source);
564
565        // Verify LineIndex matches byte_offset_to_line for all offsets
566        for offset in 0..=source.len() {
567            assert_eq!(
568                index.line_number(offset as u32),
569                byte_offset_to_line(source, offset),
570                "mismatch at offset {}",
571                offset
572            );
573        }
574    }
575
576    #[test]
577    fn test_line_index_empty_source() {
578        let source = "";
579        let index = LineIndex::new(source);
580        assert_eq!(index.line_number(0), 1);
581        assert_eq!(index.line_count(), 1);
582    }
583
584    #[test]
585    fn test_line_index_single_line() {
586        let source = "hello";
587        let index = LineIndex::new(source);
588        assert_eq!(index.line_number(0), 1);
589        assert_eq!(index.line_number(4), 1);
590        assert_eq!(index.line_count(), 1);
591    }
592
593    #[test]
594    fn test_line_index_at_newline() {
595        let source = "a\nb";
596        let index = LineIndex::new(source);
597        // At the newline character itself (offset 1)
598        assert_eq!(index.line_number(1), 1);
599        // Right after the newline (offset 2)
600        assert_eq!(index.line_number(2), 2);
601    }
602
603    #[test]
604    fn test_line_index_trailing_newline() {
605        let source = "line1\n";
606        let index = LineIndex::new(source);
607        assert_eq!(index.line_number(0), 1);
608        assert_eq!(index.line_number(5), 1); // At the newline
609        assert_eq!(index.line_number(6), 2); // After the newline
610        assert_eq!(index.line_count(), 2);
611    }
612
613    #[test]
614    fn test_line_index_span_line_number() {
615        let source = "let x = 1;\nlet y = 2;\nlet z = 3;";
616        let index = LineIndex::new(source);
617
618        let span1 = Span::new(0, 10);
619        assert_eq!(index.span_line_number(span1), 1);
620
621        let span2 = Span::new(11, 21);
622        assert_eq!(index.span_line_number(span2), 2);
623
624        let span3 = Span::new(22, 32);
625        assert_eq!(index.span_line_number(span3), 3);
626    }
627
628    #[test]
629    fn test_line_index_at_bounds() {
630        let source = "hello";
631        let index = LineIndex::new(source);
632        // At exactly the end of source
633        assert_eq!(index.line_number(5), 1);
634    }
635
636    #[test]
637    fn test_line_index_line_count() {
638        assert_eq!(LineIndex::new("").line_count(), 1);
639        assert_eq!(LineIndex::new("a").line_count(), 1);
640        assert_eq!(LineIndex::new("a\n").line_count(), 2);
641        assert_eq!(LineIndex::new("a\nb").line_count(), 2);
642        assert_eq!(LineIndex::new("a\nb\n").line_count(), 3);
643        assert_eq!(LineIndex::new("a\nb\nc").line_count(), 3);
644    }
645
646    // ========================================================================
647    // line_col tests
648    // ========================================================================
649
650    #[test]
651    fn test_byte_offset_to_line_col_basic() {
652        let source = "line1\nline2\nline3";
653        // "line1\nline2\nline3"
654        //  01234 5 6789A B CDEF0
655        // First line: offsets 0-4 are "line1", 5 is newline
656        assert_eq!(byte_offset_to_line_col(source, 0), (1, 1)); // 'l'
657        assert_eq!(byte_offset_to_line_col(source, 4), (1, 5)); // '1'
658        assert_eq!(byte_offset_to_line_col(source, 5), (1, 6)); // '\n' (still line 1)
659
660        // Second line: offsets 6-10 are "line2", 11 is newline
661        assert_eq!(byte_offset_to_line_col(source, 6), (2, 1)); // 'l'
662        assert_eq!(byte_offset_to_line_col(source, 10), (2, 5)); // '2'
663
664        // Third line: offsets 12-16 are "line3"
665        assert_eq!(byte_offset_to_line_col(source, 12), (3, 1)); // 'l'
666        assert_eq!(byte_offset_to_line_col(source, 16), (3, 5)); // '3'
667    }
668
669    #[test]
670    fn test_byte_offset_to_line_col_empty_source() {
671        let source = "";
672        assert_eq!(byte_offset_to_line_col(source, 0), (1, 1));
673    }
674
675    #[test]
676    fn test_byte_offset_to_line_col_single_line() {
677        let source = "hello";
678        assert_eq!(byte_offset_to_line_col(source, 0), (1, 1));
679        assert_eq!(byte_offset_to_line_col(source, 2), (1, 3));
680        assert_eq!(byte_offset_to_line_col(source, 4), (1, 5));
681        assert_eq!(byte_offset_to_line_col(source, 5), (1, 6)); // end of source
682    }
683
684    #[test]
685    fn test_byte_offset_to_line_col_at_newline() {
686        let source = "a\nb";
687        // offset 0: 'a' -> (1, 1)
688        // offset 1: '\n' -> (1, 2)
689        // offset 2: 'b' -> (2, 1)
690        assert_eq!(byte_offset_to_line_col(source, 0), (1, 1));
691        assert_eq!(byte_offset_to_line_col(source, 1), (1, 2));
692        assert_eq!(byte_offset_to_line_col(source, 2), (2, 1));
693    }
694
695    #[test]
696    fn test_span_line_col() {
697        let source = "let x = 1;\nlet y = 2;\nlet z = 3;";
698        // Line 1: "let x = 1;\n" (offsets 0-10, newline at 10)
699        // Line 2: "let y = 2;\n" (offsets 11-21, newline at 21)
700        // Line 3: "let z = 3;" (offsets 22-31)
701
702        let span1 = Span::new(0, 10);
703        assert_eq!(span1.line_col(source), (1, 1));
704
705        let span2 = Span::new(11, 21);
706        assert_eq!(span2.line_col(source), (2, 1));
707
708        let span3 = Span::new(22, 32);
709        assert_eq!(span3.line_col(source), (3, 1));
710
711        // Span starting in the middle of a line
712        let span_mid = Span::new(4, 10); // "x = 1;" on line 1
713        assert_eq!(span_mid.line_col(source), (1, 5)); // 'x' is at column 5
714    }
715
716    #[test]
717    fn test_line_index_line_col_basic() {
718        let source = "line1\nline2\nline3";
719        let index = LineIndex::new(source);
720
721        // First line
722        assert_eq!(index.line_col(0), (1, 1));
723        assert_eq!(index.line_col(4), (1, 5));
724
725        // Second line
726        assert_eq!(index.line_col(6), (2, 1));
727        assert_eq!(index.line_col(10), (2, 5));
728
729        // Third line
730        assert_eq!(index.line_col(12), (3, 1));
731        assert_eq!(index.line_col(16), (3, 5));
732    }
733
734    #[test]
735    fn test_line_index_line_col_matches_byte_offset() {
736        let source = "let x = 1;\nlet y = 2;\nlet z = 3;";
737        let index = LineIndex::new(source);
738
739        // Verify LineIndex matches byte_offset_to_line_col for all offsets
740        for offset in 0..=source.len() {
741            assert_eq!(
742                index.line_col(offset as u32),
743                byte_offset_to_line_col(source, offset),
744                "mismatch at offset {}",
745                offset
746            );
747        }
748    }
749
750    #[test]
751    fn test_line_index_span_line_col() {
752        let source = "let x = 1;\nlet y = 2;\nlet z = 3;";
753        let index = LineIndex::new(source);
754
755        let span1 = Span::new(0, 10);
756        assert_eq!(index.span_line_col(span1), (1, 1));
757
758        let span2 = Span::new(11, 21);
759        assert_eq!(index.span_line_col(span2), (2, 1));
760
761        let span3 = Span::new(22, 32);
762        assert_eq!(index.span_line_col(span3), (3, 1));
763
764        // Span starting in the middle of a line
765        let span_mid = Span::new(4, 10);
766        assert_eq!(index.span_line_col(span_mid), (1, 5));
767    }
768
769    #[test]
770    fn test_line_index_line_col_empty_source() {
771        let source = "";
772        let index = LineIndex::new(source);
773        assert_eq!(index.line_col(0), (1, 1));
774    }
775
776    #[test]
777    fn test_line_index_line_col_at_newline() {
778        let source = "a\nb";
779        let index = LineIndex::new(source);
780        assert_eq!(index.line_col(0), (1, 1)); // 'a'
781        assert_eq!(index.line_col(1), (1, 2)); // '\n'
782        assert_eq!(index.line_col(2), (2, 1)); // 'b'
783    }
784
785    // ========================================================================
786    // Span::contains tests
787    // ========================================================================
788
789    #[test]
790    fn test_span_contains_span() {
791        let outer = Span::new(5, 20);
792
793        // Inner span fully contained
794        assert!(outer.contains(Span::new(5, 20))); // exact match
795        assert!(outer.contains(Span::new(5, 10))); // at start
796        assert!(outer.contains(Span::new(15, 20))); // at end
797        assert!(outer.contains(Span::new(10, 15))); // in middle
798
799        // Not contained
800        assert!(!outer.contains(Span::new(0, 5))); // before (touching)
801        assert!(!outer.contains(Span::new(0, 10))); // overlaps start
802        assert!(!outer.contains(Span::new(15, 25))); // overlaps end
803        assert!(!outer.contains(Span::new(20, 25))); // after (touching)
804        assert!(!outer.contains(Span::new(0, 25))); // encompasses outer
805    }
806
807    #[test]
808    fn test_span_contains_point() {
809        let outer = Span::new(5, 20);
810
811        // Point spans (empty spans)
812        assert!(outer.contains(Span::point(5))); // at start
813        assert!(outer.contains(Span::point(10))); // in middle
814        assert!(outer.contains(Span::point(20))); // at end (point is contained)
815
816        // Point spans outside
817        assert!(!outer.contains(Span::point(4))); // before
818        assert!(!outer.contains(Span::point(21))); // after
819    }
820
821    #[test]
822    fn test_span_contains_empty_span() {
823        let empty = Span::point(10);
824
825        // Empty span only contains itself
826        assert!(empty.contains(Span::point(10)));
827        assert!(!empty.contains(Span::point(9)));
828        assert!(!empty.contains(Span::new(10, 11)));
829    }
830
831    #[test]
832    fn test_span_contains_pos() {
833        let span = Span::new(5, 10);
834
835        // Before span
836        assert!(!span.contains_pos(0));
837        assert!(!span.contains_pos(4));
838
839        // At boundaries and inside
840        assert!(span.contains_pos(5)); // start (inclusive)
841        assert!(span.contains_pos(7)); // middle
842        assert!(span.contains_pos(9)); // just before end
843        assert!(!span.contains_pos(10)); // end (exclusive)
844
845        // After span
846        assert!(!span.contains_pos(11));
847        assert!(!span.contains_pos(100));
848    }
849
850    #[test]
851    fn test_span_contains_pos_empty_span() {
852        let empty = Span::point(10);
853
854        // Empty span contains no positions (start == end)
855        assert!(!empty.contains_pos(9));
856        assert!(!empty.contains_pos(10));
857        assert!(!empty.contains_pos(11));
858    }
859}