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