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