Skip to main content

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