gruel_span/
lib.rs

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