Skip to main content

molframe_core/span/
location.rs

1//! Locations within a source file.
2//!
3//! Every parser carries a cursor that already knows its byte offset, line and
4//! column, so attaching an exact location to a diagnostic costs nothing beyond
5//! copying three integers. That is the whole reason a hand-written lexer is
6//! worth the effort over a combinator stack: the position is a by-product of
7//! scanning rather than something reconstructed afterwards.
8
9use std::fmt;
10
11/// A point in a source file.
12///
13/// `line` and `column` are 1-based, as an editor reports them. `byte_offset` is
14/// 0-based, as a slice index requires.
15#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
16pub struct Position {
17    /// Offset from the start of the input, in bytes.
18    ///
19    /// Sixty-four bits, because a reader that streams a window at a time is not
20    /// bounded by what fits in memory and an offset past four gibibytes is
21    /// ordinary rather than exceptional.
22    pub byte_offset: u64,
23    /// 1-based line number.
24    pub line: u64,
25    /// 1-based column number, counted in bytes.
26    pub column: u64,
27}
28
29impl Position {
30    /// The start of a file.
31    pub const START: Self = Self {
32        byte_offset: 0,
33        line: 1,
34        column: 1,
35    };
36
37    /// Creates a position.
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use molframe_core::Position;
43    ///
44    /// let at = Position::new(42, 3, 7);
45    /// assert_eq!(at.line, 3);
46    /// ```
47    #[must_use]
48    pub const fn new(byte_offset: u64, line: u64, column: u64) -> Self {
49        Self {
50            byte_offset,
51            line,
52            column,
53        }
54    }
55
56    /// Advances past one byte, tracking the line and column.
57    ///
58    /// A newline moves to column 1 of the next line; everything else advances
59    /// the column. Carriage returns advance the column like any other byte, so a
60    /// CRLF file reports the column of the visible character.
61    ///
62    #[must_use]
63    pub const fn advance(self, byte: u8) -> Option<Self> {
64        let Some(byte_offset) = self.byte_offset.checked_add(1) else {
65            return None;
66        };
67
68        if byte == b'\n' {
69            let Some(line) = self.line.checked_add(1) else {
70                return None;
71            };
72            Some(Self {
73                byte_offset,
74                line,
75                column: 1,
76            })
77        } else {
78            let Some(column) = self.column.checked_add(1) else {
79                return None;
80            };
81            Some(Self {
82                byte_offset,
83                line: self.line,
84                column,
85            })
86        }
87    }
88}
89
90impl fmt::Display for Position {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "{}:{}", self.line, self.column)
93    }
94}
95
96/// A half-open byte range within a source file, with the start's line and column.
97///
98/// Spans are retained on every category and value a document holds, which is
99/// what lets a write that preserves the original file reproduce untouched values
100/// byte for byte instead of re-rendering them.
101#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
102pub struct ByteSpan {
103    /// Where the span begins.
104    pub start: Position,
105    /// Offset one past the last byte of the span.
106    pub end: u64,
107}
108
109impl ByteSpan {
110    /// Creates a span running from `start` up to but excluding `end`.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use molframe_core::{ByteSpan, Position};
116    ///
117    /// let span = ByteSpan::new(Position::new(10, 2, 1), 14);
118    /// assert_eq!(span.len(), Some(4));
119    /// ```
120    #[must_use]
121    pub const fn new(start: Position, end: u64) -> Self {
122        Self { start, end }
123    }
124
125    /// Creates an empty span at a point.
126    #[must_use]
127    pub const fn empty(at: Position) -> Self {
128        Self {
129            start: at,
130            end: at.byte_offset,
131        }
132    }
133
134    /// Returns the length of the span in bytes.
135    ///
136    #[must_use]
137    pub const fn len(self) -> Option<u64> {
138        self.end.checked_sub(self.start.byte_offset)
139    }
140
141    /// Returns true when the span covers no bytes.
142    #[must_use]
143    pub const fn is_empty(self) -> bool {
144        matches!(self.len(), Some(0))
145    }
146
147    /// Returns the bytes this span covers, or `None` if it runs past the end of
148    /// `source`.
149    ///
150    /// Returning `None` rather than clamping matters: a span that does not fit
151    /// its buffer means the span and the buffer came from different reads, and
152    /// silently truncating would hide that.
153    #[must_use]
154    pub fn slice(self, source: &[u8]) -> Option<&[u8]> {
155        let start = usize::try_from(self.start.byte_offset).ok()?;
156        let end = usize::try_from(self.end).ok()?;
157
158        source.get(start..end)
159    }
160}
161
162impl fmt::Display for ByteSpan {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "{}", self.start)
165    }
166}
167
168#[cfg(test)]
169#[path = "location_tests.rs"]
170mod tests;