joule_common/
span.rs

1//! Source location tracking
2//!
3//! A `Span` represents a contiguous region of source code, used for error reporting
4//! and debugging.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// A span of source code, from `start` to `end` (exclusive) in a specific file.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct Span {
12    /// Start byte offset in the source file
13    pub start: u32,
14    /// End byte offset (exclusive)
15    pub end: u32,
16    /// File ID (from a file table)
17    pub file_id: u32,
18}
19
20impl Span {
21    /// Create a new span
22    pub const fn new(start: u32, end: u32, file_id: u32) -> Self {
23        Self {
24            start,
25            end,
26            file_id,
27        }
28    }
29
30    /// Create a dummy span (for generated code)
31    pub const fn dummy() -> Self {
32        Self {
33            start: 0,
34            end: 0,
35            file_id: 0,
36        }
37    }
38
39    /// Merge two spans (takes from start of first to end of second)
40    pub fn merge(self, other: Self) -> Self {
41        assert_eq!(
42            self.file_id, other.file_id,
43            "Cannot merge spans from different files"
44        );
45        Self {
46            start: self.start.min(other.start),
47            end: self.end.max(other.end),
48            file_id: self.file_id,
49        }
50    }
51
52    /// Create a span from the start of this span to the end of another
53    pub fn to(self, other: Self) -> Self {
54        assert_eq!(
55            self.file_id, other.file_id,
56            "Cannot combine spans from different files"
57        );
58        Self {
59            start: self.start,
60            end: other.end,
61            file_id: self.file_id,
62        }
63    }
64
65    /// Check if this span contains a byte offset
66    pub const fn contains(&self, offset: u32) -> bool {
67        offset >= self.start && offset < self.end
68    }
69
70    /// Get the length of this span in bytes
71    pub const fn len(&self) -> u32 {
72        self.end - self.start
73    }
74
75    /// Check if this span is empty
76    pub const fn is_empty(&self) -> bool {
77        self.start == self.end
78    }
79}
80
81impl Default for Span {
82    fn default() -> Self {
83        Self::dummy()
84    }
85}
86
87impl fmt::Display for Span {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        write!(f, "{}:{}-{}", self.file_id, self.start, self.end)
90    }
91}
92
93/// A value with an associated span
94#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
95pub struct Spanned<T> {
96    pub node: T,
97    pub span: Span,
98}
99
100impl<T> Spanned<T> {
101    pub const fn new(node: T, span: Span) -> Self {
102        Self { node, span }
103    }
104
105    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Spanned<U> {
106        Spanned {
107            node: f(self.node),
108            span: self.span,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_span_merge() {
119        let span1 = Span::new(10, 20, 0);
120        let span2 = Span::new(15, 30, 0);
121        let merged = span1.merge(span2);
122        assert_eq!(merged.start, 10);
123        assert_eq!(merged.end, 30);
124    }
125
126    #[test]
127    fn test_span_contains() {
128        let span = Span::new(10, 20, 0);
129        assert!(span.contains(15));
130        assert!(!span.contains(5));
131        assert!(!span.contains(25));
132    }
133
134    #[test]
135    fn test_span_len() {
136        let span = Span::new(10, 25, 0);
137        assert_eq!(span.len(), 15);
138    }
139}