partiql_common/syntax/
location.rs

1// Copyright Amazon.com, Inc. or its affiliates.
2
3//! Types representing positions, spans, locations, etc of parsed `PartiQL` text.
4
5use std::fmt;
6use std::fmt::{Debug, Display, Formatter};
7use std::num::NonZeroUsize;
8use std::ops::{Add, Range, Sub};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13macro_rules! impl_pos {
14    ($pos_type:ident, $primitive:ty) => {
15        impl Add for $pos_type {
16            type Output = Self;
17
18            fn add(self, rhs: Self) -> Self::Output {
19                Self(self.0 + rhs.0)
20            }
21        }
22        impl Add<$primitive> for $pos_type {
23            type Output = Self;
24
25            fn add(self, rhs: $primitive) -> Self::Output {
26                Self(self.0 + rhs)
27            }
28        }
29        impl Sub for $pos_type {
30            type Output = Self;
31
32            fn sub(self, rhs: Self) -> Self::Output {
33                Self(self.0 - rhs.0)
34            }
35        }
36        impl Sub<$primitive> for $pos_type {
37            type Output = Self;
38
39            fn sub(self, rhs: $primitive) -> Self::Output {
40                Self(self.0 - rhs)
41            }
42        }
43        impl $pos_type {
44            /// Constructs from a `usize`
45            #[inline(always)]
46            #[must_use]
47            pub fn from_usize(n: usize) -> Self {
48                Self(n as $primitive)
49            }
50
51            /// Converts to a `usize`
52            #[inline(always)]
53            #[must_use]
54            pub fn to_usize(&self) -> usize {
55                self.0 as usize
56            }
57        }
58        impl From<usize> for $pos_type {
59            fn from(n: usize) -> Self {
60                Self::from_usize(n)
61            }
62        }
63    };
64}
65
66/// A 0-indexed byte offset, relative to some other position.
67///
68/// This type is small (u32 currently) to allow it to be included in ASTs and other
69/// data structures.
70#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Ord, PartialOrd, Hash)]
71#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
72pub struct ByteOffset(pub u32);
73impl_pos!(ByteOffset, u32);
74
75/// A 0-indexed line offset, relative to some other position.
76#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Ord, PartialOrd, Hash)]
77#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
78pub struct LineOffset(pub u32);
79impl_pos!(LineOffset, u32);
80
81/// A 0-indexed char offset, relative to some other position.
82///
83/// This value represents the number of unicode codepoints seen, so will differ
84/// from [`ByteOffset`] for a given location in a &str if the string contains
85/// non-ASCII unicode characters
86#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Ord, PartialOrd, Hash)]
87#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
88pub struct CharOffset(pub u32);
89impl_pos!(CharOffset, u32);
90
91/// A 0-indexed byte absolute position (i.e., relative to the start of a &str)
92///
93/// This type is small (u32 currently) to allow it to be included in ASTs and other
94/// data structures.
95#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
96#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
97pub struct BytePosition(pub ByteOffset);
98
99impl From<ByteOffset> for BytePosition {
100    fn from(offset: ByteOffset) -> Self {
101        Self(offset)
102    }
103}
104
105impl From<usize> for BytePosition {
106    fn from(offset: usize) -> Self {
107        Self(offset.into())
108    }
109}
110
111impl fmt::Display for BytePosition {
112    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113        let BytePosition(ByteOffset(n)) = self;
114        write!(f, "b{n}")
115    }
116}
117
118/// A 0-indexed line & char absolute position (i.e., relative to the start of a &str)
119///
120/// ## Example
121/// ```
122/// # use partiql_common::syntax::location::LineAndCharPosition;
123/// assert_eq!("Beginning of &str: LineAndCharPosition { line: LineOffset(0), char: CharOffset(0) }",
124///             format!("Beginning of &str: {:?}", LineAndCharPosition::new(0, 0)));
125/// ```
126#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
127#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
128pub struct LineAndCharPosition {
129    /// The 0-indexed line absolute position (i.e., relative to the start of a &str)
130    pub line: LineOffset,
131    /// The 0-indexed character absolute position (i.e., relative to the start of a &str)
132    pub char: CharOffset,
133}
134
135impl LineAndCharPosition {
136    /// Constructs at [`LineAndCharPosition`]
137    #[inline]
138    #[must_use]
139    pub fn new(line: usize, char: usize) -> Self {
140        Self {
141            line: LineOffset::from_usize(line),
142            char: CharOffset::from_usize(char),
143        }
144    }
145}
146
147/// A 1-indexed line and column location intended for usage in errors/warnings/lints/etc.
148///
149/// Both line and column are 1-indexed, as that is how most people think of lines and columns.
150///
151/// ## Example
152/// ```
153/// # use partiql_common::syntax::location::LineAndColumn;
154/// assert_eq!("Beginning of &str: 1:1",format!("Beginning of &str: {}", LineAndColumn::new(1, 1).unwrap()));
155/// ```
156#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
157#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
158pub struct LineAndColumn {
159    /// The 1-indexed line absolute position (i.e., relative to the start of a &str)
160    pub line: NonZeroUsize,
161    /// The 1-indexed character absolute position (i.e., relative to the start of a &str)
162    pub column: NonZeroUsize,
163}
164
165impl LineAndColumn {
166    /// Constructs at [`LineAndColumn`] if non-zero-index invariants, else [`None`]
167    #[inline]
168    #[must_use]
169    pub fn new(line: usize, column: usize) -> Option<Self> {
170        Some(Self {
171            line: NonZeroUsize::new(line)?,
172            column: NonZeroUsize::new(column)?,
173        })
174    }
175
176    /// Constructs at [`LineAndColumn`] without verifying 1-indexed invariant (i.e. nonzero).
177    /// This results in undefined behaviour if either `line` or `column` is zero.
178    ///
179    /// # Safety
180    ///
181    /// Both `line` and `column` values must not be zero.
182    #[inline]
183    #[must_use]
184    pub const unsafe fn new_unchecked(line: usize, column: usize) -> Self {
185        Self {
186            line: NonZeroUsize::new_unchecked(line),
187            column: NonZeroUsize::new_unchecked(column),
188        }
189    }
190}
191
192impl From<LineAndCharPosition> for LineAndColumn {
193    fn from(LineAndCharPosition { line, char }: LineAndCharPosition) -> Self {
194        let line = line.to_usize() + 1;
195        let column = char.to_usize() + 1;
196        // SAFETY: +1 is added to each of line and char after upcasting from a smaller integer
197        unsafe { LineAndColumn::new_unchecked(line, column) }
198    }
199}
200
201impl fmt::Display for LineAndColumn {
202    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
203        write!(f, "{}:{}", self.line, self.column)
204    }
205}
206/// A range with an inclusive start and exclusive end.
207///
208/// Basically, a [`Range`].
209#[derive(Debug, Clone, PartialEq, Eq, Hash)]
210#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
211pub struct Location<Loc: Display> {
212    /// The start the range (inclusive).
213    pub start: Loc,
214    /// The end of the range (exclusive).
215    pub end: Loc,
216}
217
218impl<Loc> fmt::Display for Location<Loc>
219where
220    Loc: Display,
221{
222    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
223        write!(f, "(")?;
224        self.start.fmt(f)?;
225        write!(f, "..")?;
226        self.end.fmt(f)?;
227        write!(f, ")")?;
228        Ok(())
229    }
230}
231
232impl<Loc> From<Range<Loc>> for Location<Loc>
233where
234    Loc: Display,
235{
236    fn from(Range { start, end }: Range<Loc>) -> Self {
237        Location { start, end }
238    }
239}
240
241/// A wrapper type that holds an `inner` value and a `location` for it
242#[derive(Debug, Clone, PartialEq, Eq, Hash)]
243#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
244pub struct Located<T, Loc: Display> {
245    /// The item that has a location attached
246    pub inner: T,
247    /// The location of the error
248    pub location: Location<Loc>,
249}
250
251/// Trait adding a `to_located` method to ease construction of [`Located`] from its inner value.
252///
253/// ## Example
254///
255/// ```rust
256/// # use partiql_common::syntax::location::{ByteOffset, BytePosition, Located, ToLocated};
257/// assert_eq!("blah".to_string().to_located(BytePosition::from(5)..BytePosition::from(10)),
258///             Located{
259///                 inner: "blah".to_string(),
260///                 location:  (BytePosition(ByteOffset(5))..BytePosition(ByteOffset(10))).into()
261///             });
262/// ```
263pub trait ToLocated<Loc: Display>: Sized {
264    /// Create a [`Located`] from its inner value.
265    fn to_located<IntoLoc>(self, location: IntoLoc) -> Located<Self, Loc>
266    where
267        IntoLoc: Into<Location<Loc>>,
268    {
269        Located {
270            inner: self,
271            location: location.into(),
272        }
273    }
274}
275
276// "Blanket" impl of `ToLocated` for all `T`
277// See https://doc.rust-lang.org/book/ch10-02-traits.html#using-trait-bounds-to-conditionally-implement-methods
278impl<T, Loc: Display> ToLocated<Loc> for T {}
279
280impl<T, Loc: Display> Located<T, Loc> {
281    /// Maps an `Located<T, Loc>` to `Located<T, Loc2>` by applying a function to the contained
282    /// location and moving the contained `inner`
283    ///
284    /// ## Example
285    ///
286    /// ```rust
287    /// # use partiql_common::syntax::location::{ByteOffset, BytePosition, Located, ToLocated};
288    /// assert_eq!("blah".to_string()
289    ///                 .to_located(BytePosition::from(5)..BytePosition::from(10))
290    ///                 .map_loc(|BytePosition(o)| BytePosition(o+5)),
291    ///             Located{
292    ///                 inner: "blah".to_string(),
293    ///                 location: (BytePosition(ByteOffset(10))..BytePosition(ByteOffset(15))).into()
294    ///             });
295    /// ```
296    pub fn map_loc<F, Loc2>(self, mut tx: F) -> Located<T, Loc2>
297    where
298        Loc2: Display,
299        F: FnMut(Loc) -> Loc2,
300    {
301        let Located { inner, location } = self;
302        let location = Range {
303            start: tx(location.start),
304            end: tx(location.end),
305        };
306        inner.to_located(location)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use std::num::NonZeroUsize;
314
315    use crate::syntax::location::{ByteOffset, BytePosition, Located, Location};
316
317    #[test]
318    fn located() {
319        let l1: Located<String, BytePosition> = "test"
320            .to_string()
321            .to_located(ByteOffset(0).into()..ByteOffset(42).into());
322
323        assert_eq!(l1.inner, "test");
324        assert_eq!(l1.location.start.0 .0, 0);
325        assert_eq!(l1.location.end.0 .0, 42);
326        assert_eq!(l1.location.to_string(), "(b0..b42)");
327
328        let l1c = l1.clone();
329        assert!(matches!(
330            l1c,
331            Located {
332                inner: s,
333                location: Location {
334                    start:BytePosition(ByteOffset(0)),
335                    end: BytePosition(ByteOffset(42))
336                }
337            } if s == "test"
338        ));
339
340        let l2 = l1.map_loc(|x| x);
341
342        assert!(matches!(
343            l2.location,
344            Location {
345                start: BytePosition(ByteOffset(0)),
346                end: BytePosition(ByteOffset(42))
347            }
348        ));
349    }
350
351    #[test]
352    fn byteoff() {
353        let offset1 = ByteOffset(5);
354        let offset2 = ByteOffset::from_usize(15);
355
356        assert_eq!(20, (offset1 + offset2).to_usize());
357        assert_eq!(10, (offset2 - offset1).to_usize());
358        assert_eq!(ByteOffset(10), offset2 - 5);
359        assert_eq!(ByteOffset(20), offset2 + 5);
360    }
361
362    #[test]
363    fn lineoff() {
364        let offset1 = LineOffset(5);
365        let offset2 = LineOffset::from_usize(15);
366
367        assert_eq!(20, (offset1 + offset2).to_usize());
368        assert_eq!(10, (offset2 - offset1).to_usize());
369        assert_eq!(LineOffset(10), offset2 - 5);
370        assert_eq!(LineOffset(20), offset2 + 5);
371    }
372
373    #[test]
374    fn charoff() {
375        let offset1 = CharOffset(5);
376        let offset2 = CharOffset::from_usize(15);
377
378        assert_eq!(20, (offset1 + offset2).to_usize());
379        assert_eq!(10, (offset2 - offset1).to_usize());
380        assert_eq!(CharOffset(10), offset2 - 5);
381        assert_eq!(CharOffset(20), offset2 + 5);
382    }
383
384    #[test]
385    fn positions() {
386        assert_eq!(BytePosition(ByteOffset(15)), BytePosition(15.into()));
387        assert_eq!(BytePosition(ByteOffset(5)), ByteOffset(5).into());
388        assert_eq!(BytePosition(ByteOffset(25)), 25.into());
389        assert_eq!("b25", format!("{}", BytePosition(ByteOffset(25))));
390        assert_eq!("b25", BytePosition(ByteOffset(25)).to_string());
391
392        let loc = LineAndCharPosition::new(13, 42);
393        assert_eq!(
394            LineAndCharPosition {
395                line: LineOffset(13),
396                char: CharOffset(42)
397            },
398            loc
399        );
400        let display = LineAndColumn {
401            line: unsafe { NonZeroUsize::new_unchecked(14) },
402            column: unsafe { NonZeroUsize::new_unchecked(43) },
403        };
404
405        assert_eq!(display, loc.into());
406        assert_eq!(display, unsafe { LineAndColumn::new_unchecked(14, 43) });
407        assert_eq!(display, LineAndColumn::new(14, 43).unwrap());
408        assert_eq!("14:43", format!("{display}"));
409        assert_eq!("14:43", display.to_string());
410    }
411}