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;