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