Skip to main content

haskelujah_span/
lib.rs

1// For God so loved the world that he gave his only begotten Son, that whoever
2// believes in him should not perish but have eternal life. — John 3:16
3
4//! # haskelujah-span-chirho
5//!
6//! Foundation crate for source location tracking in the Haskelujah compiler.
7//! Provides file identifiers, byte offsets, and spans that all other crates
8//! use for diagnostics, error reporting, and source mapping.
9//!
10//! Zero external dependencies by design — this crate is the root of the
11//! dependency graph and must compile fast.
12
13use std::fmt;
14
15// ---------------------------------------------------------------------------
16// FileIdChirho — interned file identifier
17// ---------------------------------------------------------------------------
18
19/// A lightweight, copyable identifier for a source file.
20///
21/// File IDs are handed out by a [`SourceMapChirho`] and used inside
22/// [`SpanChirho`] values so that spans stay small (two `u32`s + a file id).
23#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct FileIdChirho(u32);
25
26impl FileIdChirho {
27    /// A sentinel value used when no real file is available (e.g. compiler-
28    /// generated code or REPL input that hasn't been assigned a file yet).
29    pub const SYNTHETIC_CHIRHO: Self = Self(u32::MAX);
30
31    /// Raw numeric value — useful for serialization or arena indexing.
32    #[inline]
33    pub fn as_raw_chirho(self) -> u32 {
34        self.0
35    }
36}
37
38impl fmt::Debug for FileIdChirho {
39    fn fmt(&self, f_chirho: &mut fmt::Formatter<'_>) -> fmt::Result {
40        if *self == Self::SYNTHETIC_CHIRHO {
41            write!(f_chirho, "FileIdChirho(SYNTHETIC)")
42        } else {
43            write!(f_chirho, "FileIdChirho({})", self.0)
44        }
45    }
46}
47
48// ---------------------------------------------------------------------------
49// ByteOffsetChirho — absolute byte position within a file
50// ---------------------------------------------------------------------------
51
52/// An absolute byte offset from the start of a source file.
53#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
54pub struct ByteOffsetChirho(u32);
55
56impl ByteOffsetChirho {
57    /// Construct from a raw `u32`.
58    #[inline]
59    pub const fn new_chirho(raw_chirho: u32) -> Self {
60        Self(raw_chirho)
61    }
62
63    /// Construct from a `usize`, saturating to `u32::MAX`.
64    #[inline]
65    pub fn from_usize_chirho(value_chirho: usize) -> Self {
66        Self(u32::try_from(value_chirho).unwrap_or(u32::MAX))
67    }
68
69    /// Raw numeric value.
70    #[inline]
71    pub const fn as_u32_chirho(self) -> u32 {
72        self.0
73    }
74
75    /// Convert to `usize` for indexing into string slices.
76    #[inline]
77    pub const fn as_usize_chirho(self) -> usize {
78        self.0 as usize
79    }
80}
81
82impl fmt::Debug for ByteOffsetChirho {
83    fn fmt(&self, f_chirho: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f_chirho, "ByteOffset({})", self.0)
85    }
86}
87
88impl fmt::Display for ByteOffsetChirho {
89    fn fmt(&self, f_chirho: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(f_chirho, "{}", self.0)
91    }
92}
93
94// ---------------------------------------------------------------------------
95// SpanChirho — a contiguous byte range within a file
96// ---------------------------------------------------------------------------
97
98/// A contiguous range of bytes `[start, end)` within a source file, identified
99/// by a [`FileIdChirho`].
100///
101/// Spans are the primary currency for diagnostics. Every AST node, token, and
102/// diagnostic label carries a span so that error messages can point to the
103/// exact source location.
104#[derive(Clone, Copy, PartialEq, Eq, Hash)]
105pub struct SpanChirho {
106    file_id_chirho: FileIdChirho,
107    start_chirho: ByteOffsetChirho,
108    end_chirho: ByteOffsetChirho,
109}
110
111impl SpanChirho {
112    /// Create a span covering `[start, end)` in the given file.
113    #[inline]
114    pub fn new_chirho(
115        file_id_chirho: FileIdChirho,
116        start_chirho: ByteOffsetChirho,
117        end_chirho: ByteOffsetChirho,
118    ) -> Self {
119        debug_assert!(
120            start_chirho <= end_chirho,
121            "SpanChirho::new_chirho requires start <= end"
122        );
123        Self {
124            file_id_chirho,
125            start_chirho,
126            end_chirho,
127        }
128    }
129
130    /// A zero-width span used as a placeholder when no real location exists.
131    pub const DUMMY_CHIRHO: Self = Self {
132        file_id_chirho: FileIdChirho::SYNTHETIC_CHIRHO,
133        start_chirho: ByteOffsetChirho::new_chirho(0),
134        end_chirho: ByteOffsetChirho::new_chirho(0),
135    };
136
137    /// The file this span belongs to.
138    #[inline]
139    pub const fn file_id_chirho(self) -> FileIdChirho {
140        self.file_id_chirho
141    }
142
143    /// Byte offset of the first byte in the span.
144    #[inline]
145    pub const fn start_chirho(self) -> ByteOffsetChirho {
146        self.start_chirho
147    }
148
149    /// Byte offset one past the last byte in the span.
150    #[inline]
151    pub const fn end_chirho(self) -> ByteOffsetChirho {
152        self.end_chirho
153    }
154
155    /// Whether this span satisfies the core invariant `start <= end`.
156    #[inline]
157    pub const fn is_valid_chirho(self) -> bool {
158        self.start_chirho.0 <= self.end_chirho.0
159    }
160
161    /// Length in bytes.
162    #[inline]
163    pub const fn len_chirho(self) -> u32 {
164        self.end_chirho.0.saturating_sub(self.start_chirho.0)
165    }
166
167    /// Whether the span covers zero bytes.
168    #[inline]
169    pub const fn is_empty_chirho(self) -> bool {
170        self.start_chirho.0 == self.end_chirho.0
171    }
172
173    /// Whether this span fully contains `other` (same file, start <=, end >=).
174    #[inline]
175    pub fn contains_chirho(self, other_chirho: SpanChirho) -> bool {
176        self.file_id_chirho == other_chirho.file_id_chirho
177            && self.start_chirho <= other_chirho.start_chirho
178            && self.end_chirho >= other_chirho.end_chirho
179    }
180
181    /// Merge two spans in the same file into the smallest span covering both.
182    /// Returns `None` if the spans belong to different files.
183    #[inline]
184    pub fn merge_chirho(self, other_chirho: SpanChirho) -> Option<SpanChirho> {
185        if self.file_id_chirho != other_chirho.file_id_chirho {
186            return None;
187        }
188        let start_chirho = if self.start_chirho < other_chirho.start_chirho {
189            self.start_chirho
190        } else {
191            other_chirho.start_chirho
192        };
193        let end_chirho = if self.end_chirho > other_chirho.end_chirho {
194            self.end_chirho
195        } else {
196            other_chirho.end_chirho
197        };
198        Some(SpanChirho::new_chirho(
199            self.file_id_chirho,
200            start_chirho,
201            end_chirho,
202        ))
203    }
204
205    /// Shrink or relocate the span. Useful when desugaring produces a node
206    /// that should point at a sub-range of the original.
207    #[inline]
208    pub const fn subspan_chirho(
209        self,
210        relative_start_chirho: u32,
211        relative_end_chirho: u32,
212    ) -> SpanChirho {
213        SpanChirho {
214            file_id_chirho: self.file_id_chirho,
215            start_chirho: ByteOffsetChirho::new_chirho(self.start_chirho.0 + relative_start_chirho),
216            end_chirho: ByteOffsetChirho::new_chirho(self.start_chirho.0 + relative_end_chirho),
217        }
218    }
219
220    /// Clamp the span end to the available source length while preserving the
221    /// `start <= end` invariant.
222    #[inline]
223    pub fn clamp_to_source_chirho(self, source_len_chirho: usize) -> SpanChirho {
224        let source_end_chirho = ByteOffsetChirho::from_usize_chirho(source_len_chirho);
225        let clamped_end_chirho = if self.end_chirho < source_end_chirho {
226            self.end_chirho
227        } else {
228            source_end_chirho
229        };
230        let clamped_end_chirho = if clamped_end_chirho < self.start_chirho {
231            self.start_chirho
232        } else {
233            clamped_end_chirho
234        };
235        SpanChirho::new_chirho(self.file_id_chirho, self.start_chirho, clamped_end_chirho)
236    }
237
238    /// Extract the spanned text from source content.
239    /// Returns `None` if the byte range is out of bounds.
240    #[inline]
241    pub fn text_chirho<'a>(&self, source_chirho: &'a str) -> Option<&'a str> {
242        let start_chirho = self.start_chirho.as_usize_chirho();
243        let end_chirho = self.end_chirho.as_usize_chirho();
244        source_chirho.get(start_chirho..end_chirho)
245    }
246}
247
248impl fmt::Debug for SpanChirho {
249    fn fmt(&self, f_chirho: &mut fmt::Formatter<'_>) -> fmt::Result {
250        write!(
251            f_chirho,
252            "Span({:?}, {}..{})",
253            self.file_id_chirho, self.start_chirho.0, self.end_chirho.0
254        )
255    }
256}
257
258impl fmt::Display for SpanChirho {
259    fn fmt(&self, f_chirho: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(f_chirho, "{}..{}", self.start_chirho.0, self.end_chirho.0)
261    }
262}
263
264// ---------------------------------------------------------------------------
265// LineIndexChirho — maps byte offsets to line/column numbers
266// ---------------------------------------------------------------------------
267
268/// A precomputed index of newline positions in a source file, enabling
269/// efficient byte-offset → line/column conversion for diagnostics.
270#[derive(Debug, Clone)]
271pub struct LineIndexChirho {
272    /// Byte offsets of the start of each line (line 0 starts at offset 0).
273    line_starts_chirho: Vec<ByteOffsetChirho>,
274}
275
276/// A human-readable line and column number (both 1-based).
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278pub struct LineColChirho {
279    /// 1-based line number.
280    pub line_chirho: u32,
281    /// 1-based column number (byte offset from line start + 1).
282    pub col_chirho: u32,
283}
284
285impl LineIndexChirho {
286    /// Build a line index from source text.
287    pub fn new_chirho(source_chirho: &str) -> Self {
288        let mut line_starts_chirho = vec![ByteOffsetChirho::new_chirho(0)];
289        for (byte_index_chirho, byte_chirho) in source_chirho.bytes().enumerate() {
290            if byte_chirho == b'\n' {
291                line_starts_chirho.push(ByteOffsetChirho::from_usize_chirho(byte_index_chirho + 1));
292            }
293        }
294        Self { line_starts_chirho }
295    }
296
297    /// Total number of lines in the source.
298    #[inline]
299    pub fn line_count_chirho(&self) -> usize {
300        self.line_starts_chirho.len()
301    }
302
303    /// Convert a byte offset to a 1-based line and column.
304    pub fn line_col_chirho(&self, offset_chirho: ByteOffsetChirho) -> LineColChirho {
305        let line_index_chirho = self
306            .line_starts_chirho
307            .partition_point(|&start_chirho| start_chirho <= offset_chirho)
308            .saturating_sub(1);
309        let line_start_chirho = self.line_starts_chirho[line_index_chirho];
310        LineColChirho {
311            line_chirho: (line_index_chirho as u32) + 1,
312            col_chirho: offset_chirho
313                .as_u32_chirho()
314                .saturating_sub(line_start_chirho.as_u32_chirho())
315                + 1,
316        }
317    }
318
319    /// Get the byte offset of the start of a 1-based line number.
320    /// Returns `None` if the line number is out of range.
321    pub fn line_start_chirho(&self, line_number_chirho: u32) -> Option<ByteOffsetChirho> {
322        let index_chirho = line_number_chirho.checked_sub(1)? as usize;
323        self.line_starts_chirho.get(index_chirho).copied()
324    }
325}
326
327// ---------------------------------------------------------------------------
328// SourceMapChirho — file registry
329// ---------------------------------------------------------------------------
330
331/// Registry that assigns [`FileIdChirho`] values and stores file names and
332/// contents. All source files loaded by the compiler are registered here so
333/// that any span can be resolved back to file name + line/column.
334#[derive(Debug, Default)]
335pub struct SourceMapChirho {
336    files_chirho: Vec<SourceEntryChirho>,
337}
338
339/// A single entry in the source map.
340#[derive(Debug)]
341struct SourceEntryChirho {
342    name_chirho: String,
343    source_chirho: String,
344    line_index_chirho: LineIndexChirho,
345}
346
347impl SourceMapChirho {
348    /// Create an empty source map.
349    pub fn new_chirho() -> Self {
350        Self::default()
351    }
352
353    /// Register a file and get back its [`FileIdChirho`].
354    pub fn add_file_chirho(
355        &mut self,
356        name_chirho: impl Into<String>,
357        source_chirho: impl Into<String>,
358    ) -> FileIdChirho {
359        let source_str_chirho = source_chirho.into();
360        let line_index_chirho = LineIndexChirho::new_chirho(&source_str_chirho);
361        let id_chirho = FileIdChirho(self.files_chirho.len() as u32);
362        self.files_chirho.push(SourceEntryChirho {
363            name_chirho: name_chirho.into(),
364            source_chirho: source_str_chirho,
365            line_index_chirho,
366        });
367        id_chirho
368    }
369
370    /// Look up the file name for a given ID.
371    pub fn file_name_chirho(&self, id_chirho: FileIdChirho) -> Option<&str> {
372        self.files_chirho
373            .get(id_chirho.0 as usize)
374            .map(|entry_chirho| entry_chirho.name_chirho.as_str())
375    }
376
377    /// Look up the full source text for a given ID.
378    pub fn file_source_chirho(&self, id_chirho: FileIdChirho) -> Option<&str> {
379        self.files_chirho
380            .get(id_chirho.0 as usize)
381            .map(|entry_chirho| entry_chirho.source_chirho.as_str())
382    }
383
384    /// Look up the line index for a given ID.
385    pub fn line_index_chirho(&self, id_chirho: FileIdChirho) -> Option<&LineIndexChirho> {
386        self.files_chirho
387            .get(id_chirho.0 as usize)
388            .map(|entry_chirho| &entry_chirho.line_index_chirho)
389    }
390
391    /// Resolve a span to file name + line/column for the start position.
392    pub fn resolve_span_start_chirho(
393        &self,
394        span_chirho: SpanChirho,
395    ) -> Option<(&str, LineColChirho)> {
396        let entry_chirho = self
397            .files_chirho
398            .get(span_chirho.file_id_chirho.0 as usize)?;
399        let line_col_chirho = entry_chirho
400            .line_index_chirho
401            .line_col_chirho(span_chirho.start_chirho);
402        Some((&entry_chirho.name_chirho, line_col_chirho))
403    }
404
405    /// Extract the source text covered by a span.
406    pub fn span_text_chirho(&self, span_chirho: SpanChirho) -> Option<&str> {
407        let source_chirho = self.file_source_chirho(span_chirho.file_id_chirho)?;
408        span_chirho.text_chirho(source_chirho)
409    }
410
411    /// Number of registered files.
412    #[inline]
413    pub fn file_count_chirho(&self) -> usize {
414        self.files_chirho.len()
415    }
416}
417
418// ---------------------------------------------------------------------------
419// Spanned<T> — a value annotated with its source span
420// ---------------------------------------------------------------------------
421
422/// A value paired with the source span where it was found.
423///
424/// This is the standard wrapper for attaching location info to parsed nodes,
425/// identifiers, literals, etc.
426#[derive(Clone, Copy, PartialEq, Eq, Hash)]
427pub struct SpannedChirho<T> {
428    /// The wrapped value.
429    pub node_chirho: T,
430    /// The source span associated with the wrapped value.
431    pub span_chirho: SpanChirho,
432}
433
434impl<T> SpannedChirho<T> {
435    /// Construct a spanned wrapper from a value and its source span.
436    #[inline]
437    pub const fn new_chirho(node_chirho: T, span_chirho: SpanChirho) -> Self {
438        Self {
439            node_chirho,
440            span_chirho,
441        }
442    }
443
444    /// Map the inner value while preserving the span.
445    #[inline]
446    pub fn map_chirho<U>(self, f_chirho: impl FnOnce(T) -> U) -> SpannedChirho<U> {
447        SpannedChirho {
448            node_chirho: f_chirho(self.node_chirho),
449            span_chirho: self.span_chirho,
450        }
451    }
452}
453
454impl<T: fmt::Debug> fmt::Debug for SpannedChirho<T> {
455    fn fmt(&self, f_chirho: &mut fmt::Formatter<'_>) -> fmt::Result {
456        write!(f_chirho, "{:?} @ {:?}", self.node_chirho, self.span_chirho)
457    }
458}
459
460// ---------------------------------------------------------------------------
461// Tests
462// ---------------------------------------------------------------------------
463
464#[cfg(test)]
465mod tests_chirho {
466    use super::*;
467
468    #[test]
469    fn file_id_debug_chirho() {
470        let id_chirho = FileIdChirho(0);
471        assert_eq!(format!("{id_chirho:?}"), "FileIdChirho(0)");
472        assert_eq!(
473            format!("{:?}", FileIdChirho::SYNTHETIC_CHIRHO),
474            "FileIdChirho(SYNTHETIC)"
475        );
476    }
477
478    #[test]
479    fn span_basics_chirho() {
480        let file_chirho = FileIdChirho(0);
481        let span_chirho = SpanChirho::new_chirho(
482            file_chirho,
483            ByteOffsetChirho::new_chirho(5),
484            ByteOffsetChirho::new_chirho(10),
485        );
486        assert_eq!(span_chirho.len_chirho(), 5);
487        assert!(!span_chirho.is_empty_chirho());
488        assert!(span_chirho.is_valid_chirho());
489        assert_eq!(span_chirho.file_id_chirho(), file_chirho);
490    }
491
492    #[test]
493    fn span_empty_chirho() {
494        let span_chirho = SpanChirho::DUMMY_CHIRHO;
495        assert!(span_chirho.is_empty_chirho());
496        assert_eq!(span_chirho.len_chirho(), 0);
497    }
498
499    #[test]
500    fn span_merge_same_file_chirho() {
501        let file_chirho = FileIdChirho(0);
502        let a_chirho = SpanChirho::new_chirho(
503            file_chirho,
504            ByteOffsetChirho::new_chirho(2),
505            ByteOffsetChirho::new_chirho(5),
506        );
507        let b_chirho = SpanChirho::new_chirho(
508            file_chirho,
509            ByteOffsetChirho::new_chirho(8),
510            ByteOffsetChirho::new_chirho(12),
511        );
512        let merged_chirho = a_chirho.merge_chirho(b_chirho).unwrap();
513        assert_eq!(
514            merged_chirho.start_chirho(),
515            ByteOffsetChirho::new_chirho(2)
516        );
517        assert_eq!(merged_chirho.end_chirho(), ByteOffsetChirho::new_chirho(12));
518    }
519
520    #[test]
521    fn span_merge_different_files_chirho() {
522        let a_chirho = SpanChirho::new_chirho(
523            FileIdChirho(0),
524            ByteOffsetChirho::new_chirho(0),
525            ByteOffsetChirho::new_chirho(5),
526        );
527        let b_chirho = SpanChirho::new_chirho(
528            FileIdChirho(1),
529            ByteOffsetChirho::new_chirho(0),
530            ByteOffsetChirho::new_chirho(5),
531        );
532        assert!(a_chirho.merge_chirho(b_chirho).is_none());
533    }
534
535    #[test]
536    fn span_contains_chirho() {
537        let file_chirho = FileIdChirho(0);
538        let outer_chirho = SpanChirho::new_chirho(
539            file_chirho,
540            ByteOffsetChirho::new_chirho(0),
541            ByteOffsetChirho::new_chirho(20),
542        );
543        let inner_chirho = SpanChirho::new_chirho(
544            file_chirho,
545            ByteOffsetChirho::new_chirho(5),
546            ByteOffsetChirho::new_chirho(15),
547        );
548        assert!(outer_chirho.contains_chirho(inner_chirho));
549        assert!(!inner_chirho.contains_chirho(outer_chirho));
550    }
551
552    #[test]
553    fn span_text_extraction_chirho() {
554        let source_chirho = "module Main where";
555        let file_chirho = FileIdChirho(0);
556        let span_chirho = SpanChirho::new_chirho(
557            file_chirho,
558            ByteOffsetChirho::new_chirho(7),
559            ByteOffsetChirho::new_chirho(11),
560        );
561        assert_eq!(span_chirho.text_chirho(source_chirho), Some("Main"));
562    }
563
564    #[test]
565    fn subspan_chirho() {
566        let file_chirho = FileIdChirho(0);
567        let span_chirho = SpanChirho::new_chirho(
568            file_chirho,
569            ByteOffsetChirho::new_chirho(10),
570            ByteOffsetChirho::new_chirho(20),
571        );
572        let sub_chirho = span_chirho.subspan_chirho(2, 5);
573        assert_eq!(sub_chirho.start_chirho(), ByteOffsetChirho::new_chirho(12));
574        assert_eq!(sub_chirho.end_chirho(), ByteOffsetChirho::new_chirho(15));
575    }
576
577    #[test]
578    fn span_clamp_to_source_chirho() {
579        let span_chirho = SpanChirho::new_chirho(
580            FileIdChirho(0),
581            ByteOffsetChirho::new_chirho(3),
582            ByteOffsetChirho::new_chirho(12),
583        );
584        let clamped_span_chirho = span_chirho.clamp_to_source_chirho(8);
585        assert!(clamped_span_chirho.is_valid_chirho());
586        assert_eq!(
587            clamped_span_chirho.end_chirho(),
588            ByteOffsetChirho::new_chirho(8)
589        );
590    }
591
592    #[test]
593    fn merge_contains_inputs_property_chirho() {
594        let file_chirho = FileIdChirho(0);
595        for start_a_raw_chirho in 0_u32..=8 {
596            for end_a_raw_chirho in start_a_raw_chirho..=8 {
597                let span_a_chirho = SpanChirho::new_chirho(
598                    file_chirho,
599                    ByteOffsetChirho::new_chirho(start_a_raw_chirho),
600                    ByteOffsetChirho::new_chirho(end_a_raw_chirho),
601                );
602
603                for start_b_raw_chirho in 0_u32..=8 {
604                    for end_b_raw_chirho in start_b_raw_chirho..=8 {
605                        let span_b_chirho = SpanChirho::new_chirho(
606                            file_chirho,
607                            ByteOffsetChirho::new_chirho(start_b_raw_chirho),
608                            ByteOffsetChirho::new_chirho(end_b_raw_chirho),
609                        );
610                        let merged_span_chirho = span_a_chirho.merge_chirho(span_b_chirho).unwrap();
611                        assert!(merged_span_chirho.contains_chirho(span_a_chirho));
612                        assert!(merged_span_chirho.contains_chirho(span_b_chirho));
613                    }
614                }
615            }
616        }
617    }
618
619    #[test]
620    fn subspan_stays_within_parent_property_chirho() {
621        let parent_span_chirho = SpanChirho::new_chirho(
622            FileIdChirho(0),
623            ByteOffsetChirho::new_chirho(10),
624            ByteOffsetChirho::new_chirho(20),
625        );
626        let parent_len_chirho = parent_span_chirho.len_chirho();
627
628        for relative_start_chirho in 0_u32..=parent_len_chirho {
629            for relative_end_chirho in relative_start_chirho..=parent_len_chirho {
630                let subspan_chirho =
631                    parent_span_chirho.subspan_chirho(relative_start_chirho, relative_end_chirho);
632                assert!(parent_span_chirho.contains_chirho(subspan_chirho));
633            }
634        }
635    }
636
637    #[test]
638    fn text_returns_none_for_out_of_bounds_spans_property_chirho() {
639        let source_chirho = "module Main where";
640        let file_chirho = FileIdChirho(0);
641        let source_len_chirho = source_chirho.len() as u32;
642
643        for start_raw_chirho in 0_u32..=source_len_chirho + 2 {
644            for extra_len_chirho in 1_u32..=3 {
645                let end_raw_chirho = start_raw_chirho + extra_len_chirho;
646                if end_raw_chirho <= source_len_chirho {
647                    continue;
648                }
649
650                let span_chirho = SpanChirho::new_chirho(
651                    file_chirho,
652                    ByteOffsetChirho::new_chirho(start_raw_chirho),
653                    ByteOffsetChirho::new_chirho(end_raw_chirho),
654                );
655                assert_eq!(span_chirho.text_chirho(source_chirho), None);
656            }
657        }
658    }
659
660    #[test]
661    fn line_index_single_line_chirho() {
662        let index_chirho = LineIndexChirho::new_chirho("hello world");
663        assert_eq!(index_chirho.line_count_chirho(), 1);
664        let lc_chirho = index_chirho.line_col_chirho(ByteOffsetChirho::new_chirho(6));
665        assert_eq!(lc_chirho.line_chirho, 1);
666        assert_eq!(lc_chirho.col_chirho, 7);
667    }
668
669    #[test]
670    fn line_index_multi_line_chirho() {
671        let source_chirho = "line one\nline two\nline three\n";
672        let index_chirho = LineIndexChirho::new_chirho(source_chirho);
673        // "line one\n" -> newline at 8, "line two\n" -> newline at 17,
674        // "line three\n" -> newline at 28. Line starts: [0, 9, 18, 29].
675        // That's 3 content lines + 1 trailing empty line = 4 starts.
676        // But our implementation counts every \n, giving us a start for
677        // the empty trailing content after the final \n.
678        let line_count_chirho = index_chirho.line_count_chirho();
679        assert!(
680            line_count_chirho == 4,
681            "expected 4 line starts, got {line_count_chirho}"
682        );
683
684        // Start of line 2 (byte offset 9 = 'l' of "line two")
685        let lc_chirho = index_chirho.line_col_chirho(ByteOffsetChirho::new_chirho(9));
686        assert_eq!(lc_chirho.line_chirho, 2);
687        assert_eq!(lc_chirho.col_chirho, 1);
688
689        // 'r' in "three" on line 3 — byte 25
690        let lc2_chirho = index_chirho.line_col_chirho(ByteOffsetChirho::new_chirho(25));
691        assert_eq!(lc2_chirho.line_chirho, 3);
692        assert_eq!(lc2_chirho.col_chirho, 8); // 25 - 18 + 1 = 8
693    }
694
695    #[test]
696    fn source_map_basics_chirho() {
697        let mut map_chirho = SourceMapChirho::new_chirho();
698        let id_chirho =
699            map_chirho.add_file_chirho("Main.hs", "module Main where\nmain = putStrLn \"hi\"\n");
700
701        assert_eq!(map_chirho.file_count_chirho(), 1);
702        assert_eq!(map_chirho.file_name_chirho(id_chirho), Some("Main.hs"));
703
704        let span_chirho = SpanChirho::new_chirho(
705            id_chirho,
706            ByteOffsetChirho::new_chirho(7),
707            ByteOffsetChirho::new_chirho(11),
708        );
709        assert_eq!(map_chirho.span_text_chirho(span_chirho), Some("Main"));
710
711        let (name_chirho, lc_chirho) = map_chirho.resolve_span_start_chirho(span_chirho).unwrap();
712        assert_eq!(name_chirho, "Main.hs");
713        assert_eq!(lc_chirho.line_chirho, 1);
714        assert_eq!(lc_chirho.col_chirho, 8);
715    }
716
717    #[test]
718    fn spanned_map_chirho() {
719        let span_chirho = SpanChirho::DUMMY_CHIRHO;
720        let spanned_chirho = SpannedChirho::new_chirho(42_i32, span_chirho);
721        let mapped_chirho = spanned_chirho.map_chirho(|n_chirho| n_chirho.to_string());
722        assert_eq!(mapped_chirho.node_chirho, "42");
723        assert_eq!(mapped_chirho.span_chirho, span_chirho);
724    }
725}