fusabi_frontend/
span.rs

1//! Source location tracking for error reporting.
2//!
3//! This module provides types for tracking source locations (spans and positions)
4//! throughout the parsing and type checking process. These are used to provide
5//! accurate error messages with source context.
6//!
7//! # Example
8//!
9//! ```rust
10//! use fusabi_frontend::span::{Position, Span};
11//!
12//! let start = Position::new(1, 5, 4);
13//! let end = Position::new(1, 10, 9);
14//! let span = Span::new(start, end);
15//!
16//! assert_eq!(span.format_location(), "line 1, column 5");
17//! ```
18
19use std::fmt;
20
21/// A position in source code.
22///
23/// Represents a single point in the source file with line, column, and byte offset.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub struct Position {
26    /// Line number (1-indexed)
27    pub line: usize,
28    /// Column number (1-indexed)
29    pub column: usize,
30    /// Byte offset in source (0-indexed)
31    pub offset: usize,
32}
33
34impl Position {
35    /// Create a new position.
36    ///
37    /// # Arguments
38    ///
39    /// * `line` - Line number (1-indexed)
40    /// * `column` - Column number (1-indexed)
41    /// * `offset` - Byte offset (0-indexed)
42    pub fn new(line: usize, column: usize, offset: usize) -> Self {
43        Position {
44            line,
45            column,
46            offset,
47        }
48    }
49
50    /// Create a position at the start of the file.
51    pub fn start() -> Self {
52        Position::new(1, 1, 0)
53    }
54}
55
56impl fmt::Display for Position {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "{}:{}", self.line, self.column)
59    }
60}
61
62/// A span of source code between two positions.
63///
64/// Represents a range in the source file, typically corresponding to a token or expression.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub struct Span {
67    /// Start position (inclusive)
68    pub start: Position,
69    /// End position (exclusive)
70    pub end: Position,
71}
72
73impl Span {
74    /// Create a new span from start and end positions.
75    pub fn new(start: Position, end: Position) -> Self {
76        Span { start, end }
77    }
78
79    /// Create a span covering a single position.
80    pub fn point(pos: Position) -> Self {
81        Span {
82            start: pos,
83            end: pos,
84        }
85    }
86
87    /// Merge two spans into a span covering both.
88    ///
89    /// The resulting span starts at the earlier start position
90    /// and ends at the later end position.
91    pub fn merge(&self, other: &Span) -> Span {
92        let start = if self.start.offset < other.start.offset {
93            self.start
94        } else {
95            other.start
96        };
97        let end = if self.end.offset > other.end.offset {
98            self.end
99        } else {
100            other.end
101        };
102        Span { start, end }
103    }
104
105    /// Format the location for error messages.
106    ///
107    /// Returns a string like "line 5, column 10" for the start position.
108    pub fn format_location(&self) -> String {
109        format!("line {}, column {}", self.start.line, self.start.column)
110    }
111
112    /// Get the length of this span in bytes.
113    pub fn len(&self) -> usize {
114        self.end.offset.saturating_sub(self.start.offset)
115    }
116
117    /// Check if this span is empty (start == end).
118    pub fn is_empty(&self) -> bool {
119        self.start.offset == self.end.offset
120    }
121
122    /// Check if this span is on a single line.
123    pub fn is_single_line(&self) -> bool {
124        self.start.line == self.end.line
125    }
126}
127
128impl fmt::Display for Span {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        if self.start.line == self.end.line {
131            write!(
132                f,
133                "{}:{}-{}",
134                self.start.line, self.start.column, self.end.column
135            )
136        } else {
137            write!(f, "{}-{}", self.start, self.end)
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    // ========================================================================
147    // Position Tests
148    // ========================================================================
149
150    #[test]
151    fn test_position_new() {
152        let pos = Position::new(5, 10, 42);
153        assert_eq!(pos.line, 5);
154        assert_eq!(pos.column, 10);
155        assert_eq!(pos.offset, 42);
156    }
157
158    #[test]
159    fn test_position_start() {
160        let pos = Position::start();
161        assert_eq!(pos.line, 1);
162        assert_eq!(pos.column, 1);
163        assert_eq!(pos.offset, 0);
164    }
165
166    #[test]
167    fn test_position_display() {
168        let pos = Position::new(10, 25, 100);
169        assert_eq!(format!("{}", pos), "10:25");
170    }
171
172    #[test]
173    fn test_position_equality() {
174        let pos1 = Position::new(1, 1, 0);
175        let pos2 = Position::new(1, 1, 0);
176        let pos3 = Position::new(1, 2, 1);
177        assert_eq!(pos1, pos2);
178        assert_ne!(pos1, pos3);
179    }
180
181    // ========================================================================
182    // Span Tests
183    // ========================================================================
184
185    #[test]
186    fn test_span_new() {
187        let start = Position::new(1, 1, 0);
188        let end = Position::new(1, 5, 4);
189        let span = Span::new(start, end);
190        assert_eq!(span.start, start);
191        assert_eq!(span.end, end);
192    }
193
194    #[test]
195    fn test_span_point() {
196        let pos = Position::new(5, 10, 42);
197        let span = Span::point(pos);
198        assert_eq!(span.start, pos);
199        assert_eq!(span.end, pos);
200        assert!(span.is_empty());
201    }
202
203    #[test]
204    fn test_span_merge_sequential() {
205        let span1 = Span::new(Position::new(1, 1, 0), Position::new(1, 5, 4));
206        let span2 = Span::new(Position::new(1, 5, 4), Position::new(1, 10, 9));
207        let merged = span1.merge(&span2);
208        assert_eq!(merged.start, Position::new(1, 1, 0));
209        assert_eq!(merged.end, Position::new(1, 10, 9));
210    }
211
212    #[test]
213    fn test_span_merge_overlapping() {
214        let span1 = Span::new(Position::new(1, 1, 0), Position::new(1, 7, 6));
215        let span2 = Span::new(Position::new(1, 5, 4), Position::new(1, 10, 9));
216        let merged = span1.merge(&span2);
217        assert_eq!(merged.start, Position::new(1, 1, 0));
218        assert_eq!(merged.end, Position::new(1, 10, 9));
219    }
220
221    #[test]
222    fn test_span_merge_reverse_order() {
223        let span1 = Span::new(Position::new(1, 5, 4), Position::new(1, 10, 9));
224        let span2 = Span::new(Position::new(1, 1, 0), Position::new(1, 5, 4));
225        let merged = span1.merge(&span2);
226        assert_eq!(merged.start, Position::new(1, 1, 0));
227        assert_eq!(merged.end, Position::new(1, 10, 9));
228    }
229
230    #[test]
231    fn test_span_format_location() {
232        let span = Span::new(Position::new(5, 10, 42), Position::new(5, 15, 47));
233        assert_eq!(span.format_location(), "line 5, column 10");
234    }
235
236    #[test]
237    fn test_span_len() {
238        let span = Span::new(Position::new(1, 1, 0), Position::new(1, 5, 4));
239        assert_eq!(span.len(), 4);
240    }
241
242    #[test]
243    fn test_span_is_empty() {
244        let pos = Position::new(1, 1, 0);
245        let empty = Span::point(pos);
246        let non_empty = Span::new(pos, Position::new(1, 5, 4));
247
248        assert!(empty.is_empty());
249        assert!(!non_empty.is_empty());
250    }
251
252    #[test]
253    fn test_span_is_single_line() {
254        let single = Span::new(Position::new(5, 1, 10), Position::new(5, 10, 19));
255        let multi = Span::new(Position::new(5, 1, 10), Position::new(6, 5, 25));
256
257        assert!(single.is_single_line());
258        assert!(!multi.is_single_line());
259    }
260
261    #[test]
262    fn test_span_display_single_line() {
263        let span = Span::new(Position::new(5, 10, 42), Position::new(5, 15, 47));
264        assert_eq!(format!("{}", span), "5:10-15");
265    }
266
267    #[test]
268    fn test_span_display_multi_line() {
269        let span = Span::new(Position::new(5, 10, 42), Position::new(7, 5, 67));
270        assert_eq!(format!("{}", span), "5:10-7:5");
271    }
272
273    #[test]
274    fn test_span_equality() {
275        let span1 = Span::new(Position::new(1, 1, 0), Position::new(1, 5, 4));
276        let span2 = Span::new(Position::new(1, 1, 0), Position::new(1, 5, 4));
277        let span3 = Span::new(Position::new(1, 1, 0), Position::new(1, 6, 5));
278
279        assert_eq!(span1, span2);
280        assert_ne!(span1, span3);
281    }
282
283    #[test]
284    fn test_span_merge_multiline() {
285        let span1 = Span::new(Position::new(1, 1, 0), Position::new(2, 5, 15));
286        let span2 = Span::new(Position::new(3, 1, 20), Position::new(4, 10, 35));
287        let merged = span1.merge(&span2);
288        assert_eq!(merged.start, Position::new(1, 1, 0));
289        assert_eq!(merged.end, Position::new(4, 10, 35));
290    }
291}