mago_span/
lib.rs

1//! Provides fundamental types for source code location tracking.
2//!
3//! This crate defines the core primitives [`Position`] and [`Span`] used throughout
4//! mago to identify specific locations in source files. It also provides
5//! the generic traits [`HasPosition`] and [`HasSpan`] to abstract over any syntax
6//! tree node or token that has a location.
7
8use std::ops::Range;
9
10use serde::Deserialize;
11use serde::Serialize;
12
13use mago_database::file::FileId;
14use mago_database::file::HasFileId;
15
16/// Represents a specific byte offset within a single source file.
17#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
18#[repr(transparent)]
19pub struct Position {
20    pub offset: u32,
21}
22
23/// Represents a contiguous range of source code within a single file.
24///
25/// A `Span` is defined by a `start` and `end` [`Position`], marking the beginning
26/// (inclusive) and end (exclusive) of a source code segment.
27#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
28pub struct Span {
29    /// The unique identifier of the file this span belongs to.
30    pub file_id: FileId,
31    /// The start position is inclusive, meaning it includes the byte at this position.
32    pub start: Position,
33    /// The end position is exclusive, meaning it does not include the byte at this position.
34    pub end: Position,
35}
36
37/// A trait for types that have a single, defined source position.
38pub trait HasPosition {
39    /// Returns the source position.
40    fn position(&self) -> Position;
41
42    /// A convenience method to get the byte offset of the position.
43    #[inline]
44    fn offset(&self) -> u32 {
45        self.position().offset
46    }
47}
48
49/// A trait for types that cover a span of source code.
50pub trait HasSpan {
51    /// Returns the source span.
52    fn span(&self) -> Span;
53
54    /// A convenience method to get the starting position of the span.
55    fn start_position(&self) -> Position {
56        self.span().start
57    }
58
59    /// A convenience method to get the starting byte offset of the span.
60    fn start_offset(&self) -> u32 {
61        self.start_position().offset
62    }
63
64    /// A convenience method to get the ending position of the span.
65    fn end_position(&self) -> Position {
66        self.span().end
67    }
68
69    /// A convenience method to get the ending byte offset of the span.
70    fn end_offset(&self) -> u32 {
71        self.end_position().offset
72    }
73}
74
75impl Position {
76    /// Creates a new `Position` from a byte offset.
77    pub const fn new(offset: u32) -> Self {
78        Self { offset }
79    }
80
81    /// Creates a new `Position` with an offset of zero.
82    pub const fn zero() -> Self {
83        Self { offset: 0 }
84    }
85
86    /// Checks if this position is at the start of a file.
87    pub const fn is_zero(&self) -> bool {
88        self.offset == 0
89    }
90
91    /// Returns a new position moved forward by the given offset.
92    ///
93    /// Uses saturating arithmetic to prevent overflow.
94    pub const fn forward(&self, offset: u32) -> Self {
95        Self { offset: self.offset.saturating_add(offset) }
96    }
97
98    /// Returns a new position moved backward by the given offset.
99    ///
100    /// Uses saturating arithmetic to prevent underflow.
101    pub const fn backward(&self, offset: u32) -> Self {
102        Self { offset: self.offset.saturating_sub(offset) }
103    }
104
105    /// Creates a `Range<u32>` starting at this position's offset with a given length.
106    pub const fn range_for(&self, length: u32) -> Range<u32> {
107        self.offset..self.offset.saturating_add(length)
108    }
109}
110
111impl Span {
112    /// Creates a new `Span` from a start and end position.
113    ///
114    /// # Panics
115    ///
116    /// In debug builds, this will panic if the start and end positions are not
117    /// from the same file (unless one is a dummy position).
118    pub const fn new(file_id: FileId, start: Position, end: Position) -> Self {
119        Self { file_id, start, end }
120    }
121
122    /// Creates a new `Span` with a zero-length, starting and ending at the same position.
123    pub const fn zero() -> Self {
124        Self { file_id: FileId::zero(), start: Position::zero(), end: Position::zero() }
125    }
126
127    /// Creates a "dummy" span with a null file ID.
128    pub fn dummy(start_offset: u32, end_offset: u32) -> Self {
129        Self::new(FileId::zero(), Position::new(start_offset), Position::new(end_offset))
130    }
131
132    /// Creates a new span that starts at the beginning of the first span
133    /// and ends at the conclusion of the second span.
134    pub fn between(start: Span, end: Span) -> Self {
135        start.join(end)
136    }
137
138    /// Checks if this span is a zero-length span, meaning it starts and ends at the same position.
139    pub const fn is_zero(&self) -> bool {
140        self.start.is_zero() && self.end.is_zero()
141    }
142
143    /// Creates a new span that encompasses both `self` and `other`.
144    /// The new span starts at `self.start` and ends at `other.end`.
145    pub fn join(self, other: Span) -> Span {
146        Span::new(self.file_id, self.start, other.end)
147    }
148
149    /// Creates a new span that starts at the beginning of this span
150    /// and ends at the specified position.
151    pub fn to_end(&self, end: Position) -> Span {
152        Span::new(self.file_id, self.start, end)
153    }
154
155    /// Creates a new span that starts at the specified position
156    /// and ends at the end of this span.
157    pub fn from_start(&self, start: Position) -> Span {
158        Span::new(self.file_id, start, self.end)
159    }
160
161    /// Creates a new span that is a subspan of this span, defined by the given byte offsets.
162    /// The `start` and `end` parameters are relative to the start of this span.
163    pub fn subspan(&self, start: u32, end: u32) -> Span {
164        Span::new(self.file_id, self.start.forward(start), self.start.forward(end))
165    }
166
167    /// Checks if a position is contained within this span's byte offsets.
168    pub fn contains(&self, position: &impl HasPosition) -> bool {
169        self.has_offset(position.offset())
170    }
171
172    /// Checks if a raw byte offset is contained within this span.
173    pub fn has_offset(&self, offset: u32) -> bool {
174        self.start.offset <= offset && offset <= self.end.offset
175    }
176
177    /// Converts the span to a `Range<u32>` of its byte offsets.
178    pub fn to_range(&self) -> Range<u32> {
179        self.start.offset..self.end.offset
180    }
181
182    /// Converts the span to a `Range<usize>` of its byte offsets.
183    pub fn to_range_usize(&self) -> Range<usize> {
184        let start = self.start.offset as usize;
185        let end = self.end.offset as usize;
186
187        start..end
188    }
189
190    /// Converts the span to a tuple of byte offsets.
191    pub fn to_offset_tuple(&self) -> (u32, u32) {
192        (self.start.offset, self.end.offset)
193    }
194
195    /// Returns the length of the span in bytes.
196    pub fn length(&self) -> u32 {
197        self.end.offset.saturating_sub(self.start.offset)
198    }
199
200    pub fn is_before(&self, other: impl HasPosition) -> bool {
201        self.end.offset <= other.position().offset
202    }
203
204    pub fn is_after(&self, other: impl HasPosition) -> bool {
205        self.start.offset >= other.position().offset
206    }
207}
208
209impl HasPosition for Position {
210    fn position(&self) -> Position {
211        *self
212    }
213}
214
215impl HasSpan for Span {
216    fn span(&self) -> Span {
217        *self
218    }
219}
220
221/// A blanket implementation that allows any `HasSpan` type to also be treated
222/// as a `HasPosition` type, using the span's start as its position.
223impl<T: HasSpan> HasPosition for T {
224    fn position(&self) -> Position {
225        self.start_position()
226    }
227}
228
229impl HasFileId for Span {
230    fn file_id(&self) -> FileId {
231        self.file_id
232    }
233}
234
235/// Ergonomic blanket impl for references.
236impl<T: HasSpan> HasSpan for &T {
237    fn span(&self) -> Span {
238        (*self).span()
239    }
240}
241
242/// Ergonomic blanket impl for boxed values.
243impl<T: HasSpan> HasSpan for Box<T> {
244    fn span(&self) -> Span {
245        self.as_ref().span()
246    }
247}
248
249impl From<Span> for Range<u32> {
250    fn from(span: Span) -> Range<u32> {
251        span.to_range()
252    }
253}
254
255impl From<&Span> for Range<u32> {
256    fn from(span: &Span) -> Range<u32> {
257        span.to_range()
258    }
259}
260
261impl From<Span> for Range<usize> {
262    fn from(span: Span) -> Range<usize> {
263        let start = span.start.offset as usize;
264        let end = span.end.offset as usize;
265
266        start..end
267    }
268}
269
270impl From<&Span> for Range<usize> {
271    fn from(span: &Span) -> Range<usize> {
272        let start = span.start.offset as usize;
273        let end = span.end.offset as usize;
274
275        start..end
276    }
277}
278
279impl From<Position> for u32 {
280    fn from(position: Position) -> u32 {
281        position.offset
282    }
283}
284
285impl From<&Position> for u32 {
286    fn from(position: &Position) -> u32 {
287        position.offset
288    }
289}
290
291impl From<u32> for Position {
292    fn from(offset: u32) -> Self {
293        Position { offset }
294    }
295}
296
297impl std::ops::Add<u32> for Position {
298    type Output = Position;
299
300    fn add(self, rhs: u32) -> Self::Output {
301        self.forward(rhs)
302    }
303}
304
305impl std::ops::Sub<u32> for Position {
306    type Output = Position;
307
308    fn sub(self, rhs: u32) -> Self::Output {
309        self.backward(rhs)
310    }
311}
312
313impl std::ops::AddAssign<u32> for Position {
314    fn add_assign(&mut self, rhs: u32) {
315        self.offset = self.offset.saturating_add(rhs);
316    }
317}
318
319impl std::ops::SubAssign<u32> for Position {
320    /// Moves the position backward in-place.
321    fn sub_assign(&mut self, rhs: u32) {
322        self.offset = self.offset.saturating_sub(rhs);
323    }
324}
325
326impl std::fmt::Display for Position {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        write!(f, "{}", self.offset)
329    }
330}
331
332impl std::fmt::Display for Span {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        write!(f, "{}..{}", self.start.offset, self.end.offset)
335    }
336}