Skip to main content

rustledger_parser/
span.rs

1//! Source location tracking.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::ops::Range;
6
7/// A span in the source code, represented as a byte range.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[cfg_attr(
10    feature = "rkyv",
11    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
12)]
13pub struct Span {
14    /// Start byte offset (inclusive).
15    pub start: usize,
16    /// End byte offset (exclusive).
17    pub end: usize,
18}
19
20impl Span {
21    /// Create a new span.
22    #[must_use]
23    pub const fn new(start: usize, end: usize) -> Self {
24        Self { start, end }
25    }
26
27    /// Create a span from a range.
28    #[must_use]
29    pub const fn from_range(range: Range<usize>) -> Self {
30        Self {
31            start: range.start,
32            end: range.end,
33        }
34    }
35
36    /// Get the length of this span in bytes.
37    #[must_use]
38    pub const fn len(&self) -> usize {
39        self.end - self.start
40    }
41
42    /// Check if the span is empty.
43    #[must_use]
44    pub const fn is_empty(&self) -> bool {
45        self.start == self.end
46    }
47
48    /// Merge this span with another, returning a span that covers both.
49    #[must_use]
50    pub fn merge(&self, other: &Self) -> Self {
51        Self {
52            start: self.start.min(other.start),
53            end: self.end.max(other.end),
54        }
55    }
56
57    /// Get the source text for this span.
58    #[must_use]
59    pub fn text<'a>(&self, source: &'a str) -> &'a str {
60        &source[self.start..self.end]
61    }
62
63    /// Convert to a chumsky span.
64    #[must_use]
65    pub const fn into_range(self) -> Range<usize> {
66        self.start..self.end
67    }
68}
69
70impl From<Range<usize>> for Span {
71    fn from(range: Range<usize>) -> Self {
72        Self::from_range(range)
73    }
74}
75
76impl From<Span> for Range<usize> {
77    fn from(span: Span) -> Self {
78        span.start..span.end
79    }
80}
81
82impl fmt::Display for Span {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "{}..{}", self.start, self.end)
85    }
86}
87
88/// Sentinel `file_id` indicating a directive was synthesized by a plugin
89/// rather than parsed from a source file.
90///
91/// Regular source files get sequential IDs starting at 0 (see
92/// `rustledger_loader::SourceMap::add_file`), so this sentinel is safely out
93/// of the normal range. Code that formats error locations or looks up files
94/// in a `SourceMap` should treat this as "no source location" and, where
95/// appropriate, hint to the user that a plugin generated the directive.
96///
97/// See issue #896.
98pub const SYNTHESIZED_FILE_ID: u16 = u16::MAX;
99
100/// A value with an associated source location (span and file).
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102#[cfg_attr(
103    feature = "rkyv",
104    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
105)]
106pub struct Spanned<T> {
107    /// The value.
108    pub value: T,
109    /// The source span (byte offsets within the file).
110    pub span: Span,
111    /// The source file ID (index into `SourceMap`).
112    /// Uses `u16` to minimize struct size (max 65,535 files).
113    pub file_id: u16,
114}
115
116impl<T> Spanned<T> {
117    /// Create a new spanned value with `file_id` defaulting to 0.
118    ///
119    /// Use `with_file_id` to set the correct file ID after creation.
120    #[must_use]
121    pub const fn new(value: T, span: Span) -> Self {
122        Self {
123            value,
124            span,
125            file_id: 0,
126        }
127    }
128
129    /// Set the file ID for this spanned value.
130    ///
131    /// Accepts `usize` for API convenience but stores as `u16` internally.
132    ///
133    /// # Panics
134    ///
135    /// Debug builds will panic if `file_id` exceeds `u16::MAX` (65,535).
136    #[must_use]
137    pub fn with_file_id(mut self, file_id: usize) -> Self {
138        debug_assert!(
139            u16::try_from(file_id).is_ok(),
140            "file_id {} exceeds u16::MAX; at most {} files are supported",
141            file_id,
142            u16::MAX
143        );
144        self.file_id = file_id as u16;
145        self
146    }
147
148    /// Map the inner value, preserving span and `file_id`.
149    #[must_use]
150    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Spanned<U> {
151        Spanned {
152            value: f(self.value),
153            span: self.span,
154            file_id: self.file_id,
155        }
156    }
157
158    /// Get a reference to the inner value.
159    #[must_use]
160    pub const fn inner(&self) -> &T {
161        &self.value
162    }
163
164    /// Unwrap the spanned value, discarding the span and `file_id`.
165    #[must_use]
166    pub fn into_inner(self) -> T {
167        self.value
168    }
169}
170
171impl<T: fmt::Display> fmt::Display for Spanned<T> {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(f, "{}", self.value)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_span_new() {
183        let span = Span::new(10, 20);
184        assert_eq!(span.start, 10);
185        assert_eq!(span.end, 20);
186    }
187
188    #[test]
189    fn test_span_from_range() {
190        let span = Span::from_range(5..15);
191        assert_eq!(span.start, 5);
192        assert_eq!(span.end, 15);
193    }
194
195    #[test]
196    fn test_span_len() {
197        let span = Span::new(10, 25);
198        assert_eq!(span.len(), 15);
199    }
200
201    #[test]
202    fn test_span_is_empty() {
203        let empty = Span::new(5, 5);
204        let non_empty = Span::new(5, 10);
205        assert!(empty.is_empty());
206        assert!(!non_empty.is_empty());
207    }
208
209    #[test]
210    fn test_span_merge() {
211        let a = Span::new(10, 20);
212        let b = Span::new(15, 30);
213        let merged = a.merge(&b);
214        assert_eq!(merged.start, 10);
215        assert_eq!(merged.end, 30);
216
217        // Test with non-overlapping spans
218        let c = Span::new(5, 8);
219        let merged2 = a.merge(&c);
220        assert_eq!(merged2.start, 5);
221        assert_eq!(merged2.end, 20);
222    }
223
224    #[test]
225    fn test_span_text() {
226        let source = "hello world";
227        let span = Span::new(0, 5);
228        assert_eq!(span.text(source), "hello");
229
230        let span2 = Span::new(6, 11);
231        assert_eq!(span2.text(source), "world");
232    }
233
234    #[test]
235    fn test_span_into_range() {
236        let span = Span::new(3, 7);
237        let range: Range<usize> = span.into_range();
238        assert_eq!(range, 3..7);
239    }
240
241    #[test]
242    fn test_span_from_impl() {
243        let span: Span = (5..10).into();
244        assert_eq!(span.start, 5);
245        assert_eq!(span.end, 10);
246    }
247
248    #[test]
249    fn test_range_from_span() {
250        let span = Span::new(2, 8);
251        let range: Range<usize> = span.into();
252        assert_eq!(range, 2..8);
253    }
254
255    #[test]
256    fn test_span_display() {
257        let span = Span::new(10, 20);
258        assert_eq!(format!("{span}"), "10..20");
259    }
260
261    #[test]
262    fn test_spanned_new() {
263        let spanned = Spanned::new("value", Span::new(0, 5));
264        assert_eq!(spanned.value, "value");
265        assert_eq!(spanned.span, Span::new(0, 5));
266    }
267
268    #[test]
269    fn test_spanned_map() {
270        let spanned = Spanned::new(5, Span::new(0, 1));
271        let mapped = spanned.map(|x| x * 2);
272        assert_eq!(mapped.value, 10);
273        assert_eq!(mapped.span, Span::new(0, 1));
274    }
275
276    #[test]
277    fn test_spanned_inner() {
278        let spanned = Spanned::new("test", Span::new(0, 4));
279        assert_eq!(spanned.inner(), &"test");
280    }
281
282    #[test]
283    fn test_spanned_into_inner() {
284        let spanned = Spanned::new(String::from("owned"), Span::new(0, 5));
285        let inner = spanned.into_inner();
286        assert_eq!(inner, "owned");
287    }
288
289    #[test]
290    fn test_spanned_display() {
291        let spanned = Spanned::new(42, Span::new(0, 2));
292        assert_eq!(format!("{spanned}"), "42");
293    }
294
295    #[test]
296    fn test_spanned_with_file_id() {
297        let spanned = Spanned::new("value", Span::new(0, 5)).with_file_id(3);
298        assert_eq!(spanned.value, "value");
299        assert_eq!(spanned.span, Span::new(0, 5));
300        assert_eq!(spanned.file_id, 3);
301    }
302}